├── logo.png
├── .img
├── docker_uuid_gen.png
└── docker_openssl_generate.png
├── .dockerignore
├── cmd
├── admin
│ ├── static
│ │ ├── img
│ │ │ ├── logo.png
│ │ │ ├── brand.png
│ │ │ ├── favicon.png
│ │ │ └── spinner.svg
│ │ ├── fonts
│ │ │ ├── osquery.eot
│ │ │ ├── osquery.ttf
│ │ │ ├── osquery.woff
│ │ │ └── osquery.woff2
│ │ ├── js
│ │ │ ├── tables.js
│ │ │ ├── login.js
│ │ │ ├── stats.js
│ │ │ ├── settings.js
│ │ │ ├── environments.js
│ │ │ ├── functions.js
│ │ │ ├── enrolls.js
│ │ │ └── profile.js
│ │ └── css
│ │ │ └── osquery.css
│ ├── oauth.go
│ ├── types-server.go
│ ├── oidc.go
│ ├── handlers
│ │ ├── json-tags.go
│ │ ├── json-stats.go
│ │ └── json-audit.go
│ ├── saml.go
│ ├── utils.go
│ ├── templates
│ │ ├── components
│ │ │ ├── page-header.html
│ │ │ ├── page-head-offline.html
│ │ │ └── page-js-offline.html
│ │ └── login.html
│ └── settings.go
├── cli
│ ├── api-audit.go
│ ├── utils.go
│ └── api-login.go
├── api
│ ├── utils.go
│ ├── handlers
│ │ ├── audit.go
│ │ ├── utils.go
│ │ ├── login.go
│ │ ├── get.go
│ │ └── platforms.go
│ ├── settings.go
│ └── auth.go
└── tls
│ ├── handlers
│ ├── get.go
│ ├── get_test.go
│ └── metrics.go
│ ├── settings.go
│ └── utils.go
├── osctrl-dev.code-workspace
├── deploy
├── config
│ ├── jwt.json
│ ├── redis.json
│ ├── service.json
│ ├── db.json
│ ├── systemd.service
│ └── tls.yaml
├── cicd
│ ├── deb
│ │ ├── deb-conffiles
│ │ ├── pre-remove.sh
│ │ ├── post-install.sh
│ │ └── pre-install.sh
│ └── docker
│ │ ├── Dockerfile-osctrl-api
│ │ ├── Dockerfile-osctrl-tls
│ │ ├── Dockerfile-osctrl-cli
│ │ └── Dockerfile-osctrl-admin
├── docker
│ ├── conf
│ │ ├── tls
│ │ │ └── openssl.cnf.example
│ │ ├── dev
│ │ │ └── air
│ │ │ │ ├── .air-osctrl-cli.toml
│ │ │ │ ├── .air-osctrl-api.toml
│ │ │ │ ├── .air-osctrl-tls.toml
│ │ │ │ └── .air-osctrl-admin.toml
│ │ ├── osquery
│ │ │ └── entrypoint.sh
│ │ ├── cli
│ │ │ └── entrypoint.sh
│ │ └── nginx
│ │ │ ├── osctrl.conf
│ │ │ └── nginx.conf
│ └── dockerfiles
│ │ ├── Dockerfile-nginx
│ │ ├── Dockerfile-osquery
│ │ ├── Dockerfile-dev-api
│ │ ├── Dockerfile-dev-tls
│ │ ├── Dockerfile-dev-admin
│ │ └── Dockerfile-dev-cli
├── osquery
│ ├── decorators.json
│ └── packs
│ │ └── osctrl-windows-application-security.conf
├── redis
│ └── redis.conf
└── nginx
│ ├── ssl.conf
│ └── nginx.conf
├── tools
├── bruno
│ ├── collection.bru
│ ├── bruno.json
│ ├── nodes
│ │ ├── get-nodes.bru
│ │ ├── get -nodes--env--all.bru
│ │ ├── get -nodes--env--active.bru
│ │ ├── get -nodes--env--inactive.bru
│ │ ├── get -nodes-node--identifier-.bru
│ │ └── post -nodes--env--delete.bru
│ ├── tags
│ │ ├── get -tags.bru
│ │ └── get -tags--env-.bru
│ ├── users
│ │ ├── get -users.bru
│ │ └── get -users--username-.bru
│ ├── settings
│ │ ├── get -settings.bru
│ │ ├── get -settings--service-.bru
│ │ ├── get -settings--service--json.bru
│ │ ├── get -settings--service---env-.bru
│ │ └── get -settings--service--json--env-.bru
│ ├── platforms
│ │ ├── get -platforms.bru
│ │ └── get -platforms--env-.bru
│ ├── environments
│ │ ├── get -environments.bru
│ │ ├── get -environments--env-.bru
│ │ ├── get -environments--env--enroll--target-.bru
│ │ ├── get -environments--env--remove--target-.bru
│ │ ├── post -environments--env--enroll--target-.bru
│ │ ├── post -environments--env--remove--action-.bru
│ │ └── post -environments--env--enroll--action-.bru
│ └── queries
│ │ ├── post -carves--env-.bru
│ │ ├── get -carves--env-.bru
│ │ ├── get -queries--env-.bru
│ │ ├── get -all-queries--env-.bru
│ │ ├── get -carves--env---name-.bru
│ │ ├── get -queries--env---name-.bru
│ │ ├── get -queries--env--results--name-.bru
│ │ ├── post - create queries.bru
│ │ └── post -queries--env-.bru
├── .gitignore
├── update-modules.sh
└── fake_logging.py
├── .flake8
├── pkg
├── version
│ ├── version.go
│ └── version_test.go
├── cache
│ ├── utils.go
│ ├── cache.go
│ └── metrics.go
├── queries
│ ├── utils.go
│ └── saved.go
├── users
│ ├── utils.go
│ └── utils_test.go
├── logging
│ ├── debughttp.go
│ ├── utils.go
│ ├── dispatch.go
│ ├── stdout.go
│ ├── none.go
│ ├── s3.go
│ ├── file.go
│ └── kinesis.go
├── carves
│ ├── db.go
│ └── utils.go
├── environments
│ ├── decorators.go
│ ├── env-cache.go
│ ├── flags_test.go
│ ├── oneliners_test.go
│ └── util.go
├── utils
│ ├── string-utils_test.go
│ ├── utils_test.go
│ ├── string-utils.go
│ ├── utils.go
│ └── time-utils.go
├── auditlog
│ ├── utils_test.go
│ └── utils.go
├── tags
│ └── utils_test.go
└── nodes
│ ├── models.go
│ ├── node-cache.go
│ └── utils.go
├── CHANGELOG.md
├── .env.example
├── .golangci.yml
├── .pre-commit-config.yaml
├── .gitignore
├── LICENSE
├── .github
├── workflows
│ ├── test-release.yml
│ ├── golangci-lint.yml
│ ├── build_and_test_pr.yml
│ └── release.yml
└── actions
│ ├── build
│ └── binaries
│ │ └── action.yml
│ ├── test
│ └── binaries
│ │ └── action.yml
│ └── tagged_release
│ ├── docker
│ └── codesign
│ │ └── action.yml
│ └── github
│ └── action.yml
├── CONTRIBUTING.md
└── README.md
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/logo.png
--------------------------------------------------------------------------------
/.img/docker_uuid_gen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/.img/docker_uuid_gen.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | *.md
2 | LICENSE
3 | Vagrantfile
4 | *.png
5 | tmp/
6 | bin/
7 | docker-compose-dev.yml
8 |
--------------------------------------------------------------------------------
/cmd/admin/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/img/logo.png
--------------------------------------------------------------------------------
/.img/docker_openssl_generate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/.img/docker_openssl_generate.png
--------------------------------------------------------------------------------
/cmd/admin/static/img/brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/img/brand.png
--------------------------------------------------------------------------------
/cmd/admin/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/img/favicon.png
--------------------------------------------------------------------------------
/cmd/admin/static/fonts/osquery.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/fonts/osquery.eot
--------------------------------------------------------------------------------
/cmd/admin/static/fonts/osquery.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/fonts/osquery.ttf
--------------------------------------------------------------------------------
/osctrl-dev.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {},
8 | }
--------------------------------------------------------------------------------
/cmd/admin/static/fonts/osquery.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/fonts/osquery.woff
--------------------------------------------------------------------------------
/cmd/admin/static/fonts/osquery.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmpsec/osctrl/HEAD/cmd/admin/static/fonts/osquery.woff2
--------------------------------------------------------------------------------
/deploy/config/jwt.json:
--------------------------------------------------------------------------------
1 | {
2 | "jwt": {
3 | "jwtSecret": "_JWT_SECRET",
4 | "hoursToExpire": 3
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/deploy/cicd/deb/deb-conffiles:
--------------------------------------------------------------------------------
1 | /opt/osctrl/config/{{ OSCTRL_COMPONENT }}.json
2 | /etc/systemd/system/osctrl-{{ OSCTRL_COMPONENT }}.service
3 |
--------------------------------------------------------------------------------
/tools/bruno/collection.bru:
--------------------------------------------------------------------------------
1 | vars:pre-request {
2 | env: 1a026f60-edc1-4189-ab70-be99d541a473
3 | baseUrl: http://localhost:9002
4 | }
5 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | __pycache__,
5 | docs/source/conf.py,
6 | old,
7 | build,
8 | dist
9 | max-line-length = 100
10 |
--------------------------------------------------------------------------------
/deploy/config/redis.json:
--------------------------------------------------------------------------------
1 | {
2 | "redis": {
3 | "host": "_REDIS_HOST",
4 | "port": "_REDIS_PORT",
5 | "password": "_REDIS_PASSWORD",
6 | "db": 0
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tools/bruno/bruno.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1",
3 | "name": "osctrl-api",
4 | "type": "collection",
5 | "ignore": [
6 | "node_modules",
7 | ".git"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/get-nodes.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get-nodes
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: {{baseUrl}} /api/v1/nodes/{{env}}/all
9 | body: none
10 | auth: none
11 | }
12 |
--------------------------------------------------------------------------------
/deploy/cicd/deb/pre-remove.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | ################### Remove osctrl user and group ###################
6 | if id -u osctrl &>/dev/null
7 | then
8 | deluser --remove-home osctrl
9 | fi
10 |
--------------------------------------------------------------------------------
/pkg/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | const (
4 | // OsctrlVersion to have the version for all components
5 | OsctrlVersion = "0.4.8"
6 | // OsqueryVersion to have the version for osquery defined
7 | OsqueryVersion = "5.20.0"
8 | )
9 |
--------------------------------------------------------------------------------
/tools/bruno/tags/get -tags.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -tags
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/tags
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/users/get -users.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -users
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/users
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/settings/get -settings.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -settings
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/settings
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/deploy/config/service.json:
--------------------------------------------------------------------------------
1 | {
2 | "_SERVICE_NAME": {
3 | "listener": "_LISTENER",
4 | "port": "_SERVICE_PORT",
5 | "host": "_SERVICE_HOST",
6 | "auth": "_SERVICE_AUTH",
7 | "logger": "_SERVICE_LOGGING",
8 | "carver": "_SERVICE_CARVER"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tools/bruno/platforms/get -platforms.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -platforms
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/platforms
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/environments/get -environments.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -environments
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/environments
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/queries/post -carves--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -carves--env-
3 | type: http
4 | seq: 7
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/carves/{env}
9 | body: json
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/cache/utils.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jmpsec/osctrl/pkg/config"
7 | )
8 |
9 | // PrepareAddr to generate redis connection string
10 | func PrepareAddr(cfg config.YAMLConfigurationRedis) string {
11 | return fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
12 | }
13 |
--------------------------------------------------------------------------------
/tools/bruno/tags/get -tags--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -tags--env-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/tags/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -carves--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -carves--env-
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/carves/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -queries--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -queries--env-
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/queries/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/get -nodes--env--all.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -nodes--env--all
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/nodes/{env}/all
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/platforms/get -platforms--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -platforms--env-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/platforms/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/version/version_test.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestOsqueryVersion(t *testing.T) {
10 | assert.Equal(t, "5.20.0", OsqueryVersion)
11 | }
12 |
13 | func TestOsctrlVersion(t *testing.T) {
14 | assert.Equal(t, "0.4.8", OsctrlVersion)
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/get -nodes--env--active.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -nodes--env--active
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/nodes/{env}/active
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -all-queries--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -all-queries--env-
3 | type: http
4 | seq: 5
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/all-queries/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/users/get -users--username-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -users--username-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/users/{username}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | username:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/environments/get -environments--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -environments--env-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/environments/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/get -nodes--env--inactive.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -nodes--env--inactive
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/nodes/{env}/inactive
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/settings/get -settings--service-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -settings--service-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/settings/{service}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | service:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -carves--env---name-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -carves--env---name-
3 | type: http
4 | seq: 8
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/carves/{env}/{name}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | name:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/get -nodes-node--identifier-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -nodes-node--identifier-
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/nodes/node/{identifier}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | identifier:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -queries--env---name-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -queries--env---name-
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/queries/{env}/{name}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | name:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/settings/get -settings--service--json.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -settings--service--json
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/settings/{service}/json
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | service:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/queries/get -queries--env--results--name-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -queries--env--results--name-
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/queries/{env}/results/{name}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | name:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/settings/get -settings--service---env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -settings--service---env-
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/settings/{service}/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | service:
15 | env:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/cmd/admin/oauth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // JSONConfigurationOAuth to keep all OAuth details for auth
4 | type JSONConfigurationOAuth struct {
5 | ClientID string `json:"clientid"`
6 | ClientSecret string `json:"clientsecret"`
7 | RedirectURL string `json:"redirecturl"`
8 | Scopes []string `json:"scopes"`
9 | EndpointURL string `json:"endpointurl"`
10 | }
11 |
--------------------------------------------------------------------------------
/deploy/config/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "db": {
3 | "type": "postgres",
4 | "host": "_DB_HOST",
5 | "port": "_DB_PORT",
6 | "name": "_DB_NAME",
7 | "username": "_DB_USERNAME",
8 | "password": "_DB_PASSWORD",
9 | "sslmode": "disable",
10 | "maxIdleConns": 20,
11 | "maxOpenConns": 100,
12 | "connMaxLifetime": 30,
13 | "connRetry": 10
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tools/bruno/queries/post - create queries.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post - create queries
3 | type: http
4 | seq: 9
5 | }
6 |
7 | post {
8 | url: {{baseUrl}}/api/v1/queries/{{env}}
9 | body: json
10 | auth: none
11 | }
12 |
13 | body:json {
14 | {
15 | "environment_list": ["dev"],
16 | "query": "SELECT * FROM system_info;",
17 | "exp_hours": 1
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tools/bruno/settings/get -settings--service--json--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -settings--service--json--env-
3 | type: http
4 | seq: 5
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/settings/{service}/json/{env}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | service:
15 | env:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/queries/post -queries--env-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -queries--env-
3 | type: http
4 | seq: 2
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/queries/{env}
9 | body: json
10 | auth: bearer
11 | }
12 |
13 | auth:bearer {
14 | token: {{token}}
15 | }
16 |
17 | body:json {
18 | {
19 | "uuid": "",
20 | "query": "",
21 | "hidden": ""
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # osctrl Changelog
2 |
3 | ## 🔖 Release [0.4.8](https://github.com/jmpsec/osctrl/releases/tag/v0.4.8)
4 |
5 | ### 🚨 Breaking Changes
6 |
7 | ### ✨ New Features
8 |
9 | ### 🛠 Improvements and ⚡️ Performance
10 |
11 | ### 🐛 Bug Fixes
12 |
13 | ### 🔒 Security
14 |
15 | ### ♻️ Refactoring and 🏗 Chores
16 |
17 | ### 📚 Documentation
18 |
19 | ### 📦 Build
20 |
21 | ### 🚦 Test
22 |
--------------------------------------------------------------------------------
/tools/bruno/environments/get -environments--env--enroll--target-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -environments--env--enroll--target-
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/environments/{env}/enroll/{target}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | target:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/environments/get -environments--env--remove--target-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: get -environments--env--remove--target-
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: https://osctrl.net/api/v1/environments/{env}/remove/{target}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | target:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/environments/post -environments--env--enroll--target-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -environments--env--enroll--target-
3 | type: http
4 | seq: 4
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/environments/{env}/enroll/{target}
9 | body: none
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | target:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/environments/post -environments--env--remove--action-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -environments--env--remove--action-
3 | type: http
4 | seq: 7
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/environments/{env}/remove/{action}
9 | body: json
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | action:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
--------------------------------------------------------------------------------
/tools/bruno/nodes/post -nodes--env--delete.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -nodes--env--delete
3 | type: http
4 | seq: 5
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/nodes/{env}/delete
9 | body: json
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | }
16 |
17 | auth:bearer {
18 | token: {{token}}
19 | }
20 |
21 | body:json {
22 | {
23 | "uuid": ""
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/deploy/docker/conf/tls/openssl.cnf.example:
--------------------------------------------------------------------------------
1 | [req]
2 | default_bits = 4096
3 | prompt = no
4 | default_md = sha256
5 | x509_extensions = v3_req
6 | distinguished_name = dn
7 |
8 | [dn]
9 | emailAddress = dev@${ENV:BASE_DOMAIN}
10 | CN = osctrl.${ENV:BASE_DOMAIN}
11 |
12 | [v3_req]
13 | subjectAltName = @alt_names
14 |
15 | [alt_names]
16 | DNS.1 = osctrl.${ENV:BASE_DOMAIN}
17 | DNS.2 = osctrl-nginx
18 | DNS.3 = nginx
19 | DNS.4 = osctrl
20 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-nginx:
--------------------------------------------------------------------------------
1 | ARG NGINX_VERSION
2 | FROM nginx:${NGINX_VERSION}
3 |
4 | ### Copy NGINX configs ###
5 | COPY deploy/docker/conf/nginx/nginx.conf /etc/nginx/nginx.conf
6 | COPY deploy/docker/conf/nginx/osctrl.conf /etc/nginx/conf.d/osctrl.conf
7 |
8 | ### Copy TLS public cert and private key ###
9 | COPY deploy/docker/conf/tls/*.crt /etc/ssl/certs/osctrl.crt
10 | COPY deploy/docker/conf/tls/*.key /etc/ssl/private/osctrl.key
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | OSCTRL_VERSION=0.4.8
2 | OSQUERY_VERSION=5.20.0
3 | NGINX_VERSION=1.21.6-alpine
4 | POSTGRES_VERSION=13.5-alpine
5 | POSTGRES_DB_NAME=osctrl
6 | POSTGRES_DB_USERNAME=osctrl
7 | POSTGRES_DB_PASSWORD=osctrl
8 | REDIS_VERSION=6.2.6-alpine3.15
9 | JWT_SECRET=0000000000000000000000000000000000000000000000000000000000000000
10 | SESSION_KEY=sessionkey
11 | OSCTRL_USER=admin
12 | OSCTRL_PASS=Changeme123!
13 | GOLANG_VERSION=1.25.4
14 | OSCTRL_TLS_LOGGER=db
15 |
--------------------------------------------------------------------------------
/pkg/queries/utils.go:
--------------------------------------------------------------------------------
1 | package queries
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/jmpsec/osctrl/pkg/utils"
7 | )
8 |
9 | // Helper to generate a random query name
10 | func GenQueryName() string {
11 | return "query_" + utils.RandomForNames()
12 | }
13 |
14 | // Helper to generate the time.Time for the expiration of a query or carve based on hours
15 | func QueryExpiration(exp int) time.Time {
16 | return time.Now().Add(time.Duration(exp) * time.Hour)
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/admin/types-server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // JSONAdminUsers to keep all admin users for auth JSON
4 | type JSONAdminUsers struct {
5 | Username string `json:"username"`
6 | Password string `json:"password"`
7 | Fullname string `json:"fullname"`
8 | Admin bool `json:"admin"`
9 | }
10 |
11 | // JWTData to return all the fields from a JWT token
12 | type JWTData struct {
13 | Subject string
14 | Email string
15 | Display string
16 | Username string
17 | }
18 |
--------------------------------------------------------------------------------
/tools/.gitignore:
--------------------------------------------------------------------------------
1 | # Files used by packages
2 | osquery.flags
3 | osquery.secret
4 |
5 | # Generated packages
6 | *.pkg
7 | *.deb
8 |
9 | # Ignore temporary directories matching the date format YYYYMMDD-HHMMSS and YYYYMMDD-HHMMSS-scripts
10 | [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]-[0-2][0-9][0-5][0-9][0-5][0-9]/
11 | [0-9][0-9][0-9][0-9][0-1][0-9][0-3][0-9]-[0-2][0-9][0-5][0-9][0-5][0-9]-scripts/
12 |
13 | # Fake news files
14 | fake_news_go/fake_news
15 | fake_news_go/state.json
16 | fake_news_go/http_errors.log
17 |
--------------------------------------------------------------------------------
/cmd/admin/oidc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // JSONConfigurationOIDC to keep all OIDC details for auth
4 | type JSONConfigurationOIDC struct {
5 | IssuerURL string `json:"issuerurl"`
6 | ClientID string `json:"clientid"`
7 | ClientSecret string `json:"clientsecret"`
8 | RedirectURL string `json:"redirecturl"`
9 | Scope []string `json:"scope"`
10 | Nonce string `json:"nonce"`
11 | ResponseType string `json:"responsetype"`
12 | AuthorizationCode string `json:"authorizationcode"`
13 | }
14 |
--------------------------------------------------------------------------------
/tools/bruno/environments/post -environments--env--enroll--action-.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: post -environments--env--enroll--action-
3 | type: http
4 | seq: 5
5 | }
6 |
7 | post {
8 | url: https://osctrl.net/api/v1/environments/{env}/enroll/{action}
9 | body: json
10 | auth: bearer
11 | }
12 |
13 | params:path {
14 | env:
15 | action:
16 | }
17 |
18 | auth:bearer {
19 | token: {{token}}
20 | }
21 |
22 | body:json {
23 | {
24 | "Certificate": "",
25 | "MacPkgURL": "",
26 | "MsiPkgURL": "",
27 | "RpmPkgURL": "",
28 | "DebPkgURL": ""
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | run:
3 | issues-exit-code: 1
4 | linters:
5 | enable:
6 | - errorlint
7 | - gocritic
8 | - misspell
9 | exclusions:
10 | generated: lax
11 | presets:
12 | - comments
13 | - common-false-positives
14 | - legacy
15 | - std-error-handling
16 | paths:
17 | - third_party$
18 | - builtin$
19 | - examples$
20 | formatters:
21 | enable:
22 | - gofmt
23 | - goimports
24 | exclusions:
25 | generated: lax
26 | paths:
27 | - third_party$
28 | - builtin$
29 | - examples$
30 |
--------------------------------------------------------------------------------
/deploy/osquery/decorators.json:
--------------------------------------------------------------------------------
1 | {
2 | "always": [
3 | "SELECT username AS osquery_user FROM users WHERE uid = (SELECT uid FROM processes WHERE pid = (SELECT pid FROM osquery_info) LIMIT 1);",
4 | "SELECT hostname, local_hostname FROM system_info;",
5 | "SELECT user || ' (' || tty || ')' AS username FROM logged_in_users WHERE type = 'user' ORDER BY time LIMIT 1;",
6 | "SELECT version AS osquery_version, config_hash FROM osquery_info WHERE config_valid = 1;",
7 | "SELECT md5 AS osquery_md5 FROM hash WHERE path = (SELECT path FROM processes WHERE pid = (SELECT pid FROM osquery_info));"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-osquery:
--------------------------------------------------------------------------------
1 | ARG OSCTRL_VERSION
2 | FROM jmpsec/osctrl-cli:v${OSCTRL_VERSION}
3 |
4 | ARG OSQUERY_VERSION=5.20.0
5 |
6 | USER root
7 |
8 | # Install Osquery
9 | RUN apt-get update -y -qq && apt-get install -y curl host
10 | RUN curl -L https://pkg.osquery.io/deb/osquery_${OSQUERY_VERSION}-1.linux_amd64.deb \
11 | --output /tmp/osquery_${OSQUERY_VERSION}-1.linux_amd64.deb
12 | RUN dpkg -i /tmp/osquery_${OSQUERY_VERSION}-1.linux_amd64.deb
13 |
14 |
15 | # Entrypoint
16 | COPY deploy/docker/conf/osquery/entrypoint.sh /entrypoint.sh
17 | RUN chmod 755 /entrypoint.sh
18 | ENTRYPOINT [ "/entrypoint.sh" ]
19 |
--------------------------------------------------------------------------------
/deploy/config/systemd.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=_NAME
3 | ConditionPathExists=_DEST
4 | After=network.target
5 |
6 | [Service]
7 | Type=simple
8 | User=_UU
9 | Group=_GG
10 | Restart=on-failure
11 | RestartSec=10
12 |
13 | WorkingDirectory=_DEST
14 | ExecStart=_DEST/_NAME _ARGS
15 |
16 | # make sure log directory exists and owned by syslog
17 | PermissionsStartOnly=true
18 | ExecStartPre=/bin/mkdir -p /var/log/_NAME
19 | ExecStartPre=/bin/chown _UU:_GG /var/log/_NAME
20 | ExecStartPre=/bin/chmod 755 /var/log/_NAME
21 | StandardOutput=syslog
22 | StandardError=syslog
23 | SyslogIdentifier=_NAME
24 |
25 | [Install]
26 | WantedBy=multi-user.target
27 |
--------------------------------------------------------------------------------
/pkg/users/utils.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | // Helper to compare two set of permissions
4 | func SameAccess(acc1, acc2 EnvAccess) bool {
5 | return (acc1.Admin == acc2.Admin) && (acc1.Query == acc2.Query) && (acc1.Carve == acc2.Carve) && (acc1.User == acc2.User)
6 | }
7 |
8 | // Helper to convert received permissions into struct
9 | func GenEnvAccess(admin, carve, query, user bool) EnvAccess {
10 | if admin {
11 | return EnvAccess{
12 | User: true,
13 | Query: true,
14 | Carve: true,
15 | Admin: true,
16 | }
17 | }
18 | return EnvAccess{
19 | User: user,
20 | Query: query,
21 | Carve: carve,
22 | Admin: admin,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/tables.js:
--------------------------------------------------------------------------------
1 | // Function to pause/refresh the table fetching data
2 | function changeTableRefresh(value_id, button_id) {
3 | if (document.getElementById(value_id).value === 'yes') {
4 | document.getElementById(value_id).value = 'no';
5 | document.getElementById(button_id).innerHTML = '';
6 | } else {
7 | document.getElementById(value_id).value = 'yes';
8 | document.getElementById(button_id).innerHTML = '';
9 | }
10 | return;
11 | }
12 |
13 | // Function to refresh table when clicked
14 | function refreshTableNow(table_id) {
15 | var table = $('#' + table_id).DataTable();
16 | table.ajax.reload();
17 | return;
18 | }
19 |
--------------------------------------------------------------------------------
/cmd/cli/api-audit.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "path"
7 |
8 | "github.com/jmpsec/osctrl/pkg/auditlog"
9 | )
10 |
11 | // GetAuditLogs to retrieve all audit logs from osctrl
12 | func (api *OsctrlAPI) GetAuditLogs() ([]auditlog.AuditLog, error) {
13 | var als []auditlog.AuditLog
14 | reqURL := fmt.Sprintf("%s%s", api.Configuration.URL, path.Join(APIPath, APIAuditLogs))
15 | rawAls, err := api.GetGeneric(reqURL, nil)
16 | if err != nil {
17 | return als, fmt.Errorf("error api request - %w - %s", err, string(rawAls))
18 | }
19 | if err := json.Unmarshal(rawAls, &als); err != nil {
20 | return als, fmt.Errorf("can not parse body - %w", err)
21 | }
22 | return als, nil
23 | }
24 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-dev-api:
--------------------------------------------------------------------------------
1 | ARG GOLANG_VERSION=${GOLANG_VERSION:-1.25.4}
2 | FROM golang:${GOLANG_VERSION} AS osctrl-api-dev
3 |
4 | WORKDIR /usr/src/app
5 |
6 | ENV GO111MODULE="on"
7 | ENV GOOS="linux"
8 | ENV CGO_ENABLED=0
9 |
10 | # Hot reloading mod
11 | RUN go install github.com/cosmtrek/air@v1.41.0
12 | RUN go install github.com/go-delve/delve/cmd/dlv@v1.20.1
13 |
14 | # Copy code
15 | COPY . /usr/src/app
16 |
17 | # Download deps
18 | RUN go mod download
19 | RUN go mod verify
20 |
21 | ### Copy osctrl-api bin and configs ###
22 | RUN mkdir -p /opt/osctrl/bin
23 | RUN mkdir -p /opt/osctrl/config
24 |
25 | EXPOSE 9002
26 | ENTRYPOINT ["air", "-c", "deploy/docker/conf/dev/air/.air-osctrl-api.toml"]
27 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-dev-tls:
--------------------------------------------------------------------------------
1 | ARG GOLANG_VERSION=${GOLANG_VERSION:-1.25.4}
2 | FROM golang:${GOLANG_VERSION} AS osctrl-tls-dev
3 |
4 | WORKDIR /usr/src/app
5 |
6 | ENV GO111MODULE="on"
7 | ENV GOOS="linux"
8 | ENV CGO_ENABLED=0
9 |
10 | # Hot reloading mod
11 | RUN go install github.com/cosmtrek/air@v1.41.0
12 | RUN go install github.com/go-delve/delve/cmd/dlv@v1.20.1
13 |
14 | # Copy code
15 | COPY . /usr/src/app
16 |
17 | # Download deps
18 | RUN go mod download
19 | RUN go mod verify
20 |
21 | ### Copy osctrl-api bin and configs ###
22 | RUN mkdir -p /opt/osctrl/bin
23 | RUN mkdir -p /opt/osctrl/config
24 |
25 | EXPOSE 9000
26 | ENTRYPOINT ["air", "-c", "deploy/docker/conf/dev/air/.air-osctrl-tls.toml"]
27 |
--------------------------------------------------------------------------------
/cmd/api/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/jmpsec/osctrl/pkg/config"
4 |
5 | // Helper to compose paths for API
6 | func _apiPath(target string) string {
7 | return apiPrefixPath + apiVersionPath + target
8 | }
9 |
10 | // Helper to convert YAML settings loaded from file to settings
11 | func loadedYAMLToServiceParams(yml config.APIConfiguration, loadedFile string) *config.ServiceParameters {
12 | return &config.ServiceParameters{
13 | ConfigFlag: true,
14 | ServiceConfigFile: loadedFile,
15 | Service: &yml.Service,
16 | DB: &yml.DB,
17 | Redis: &yml.Redis,
18 | JWT: &yml.JWT,
19 | TLS: &yml.TLS,
20 | Debug: &yml.Debug,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/login.js:
--------------------------------------------------------------------------------
1 | function sendLogin() {
2 | var _user = $("#login_user").val();
3 | var _password = $("#login_password").val();
4 |
5 | var _url = '/login';
6 | var data = {
7 | username: _user,
8 | password: _password
9 | };
10 | sendPostRequest(data, _url, '', false, function(_data){
11 | window.location.replace(_data.message);
12 | });
13 | }
14 |
15 | function sendLogout() {
16 | var _csrf = $("#csrftoken").val();
17 |
18 | var _url = '/logout';
19 | var data = {
20 | csrftoken: _csrf
21 | };
22 | sendPostRequest(data, _url, '/logout', false);
23 | }
24 |
25 | $("#login_password").keyup(function(event) {
26 | if (event.keyCode === 13) {
27 | $("#login_button").click();
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/deploy/cicd/docker/Dockerfile-osctrl-api:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ARG COMPONENT=api
4 | ARG GOOS=linux
5 | ARG GOARCH=amd64
6 |
7 | # Install software
8 | RUN apt-get update -y -q && \
9 | rm -rf /var/lib/apt/lists/*
10 |
11 | # Install/Setup osctrl
12 | RUN useradd -ms /usr/sbin/nologin osctrl-${COMPONENT}
13 | RUN mkdir -p /opt/osctrl/bin && \
14 | mkdir -p /opt/osctrl/config && \
15 | mkdir -p /opt/osctrl/script && \
16 | chown osctrl-${COMPONENT}:osctrl-${COMPONENT} -R /opt/osctrl
17 | COPY osctrl-${COMPONENT}-${GOOS}-${GOARCH}.bin /opt/osctrl/bin/osctrl-${COMPONENT}
18 | RUN chmod 755 /opt/osctrl/bin/osctrl-${COMPONENT}
19 | USER osctrl-${COMPONENT}
20 | WORKDIR /opt/osctrl
21 | EXPOSE 9002/tcp
22 | CMD ["/opt/osctrl/bin/osctrl-api"]
23 |
--------------------------------------------------------------------------------
/deploy/cicd/docker/Dockerfile-osctrl-tls:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ARG COMPONENT=tls
4 | ARG GOOS=linux
5 | ARG GOARCH=amd64
6 |
7 | # Install software
8 | RUN apt-get update -y -q && \
9 | rm -rf /var/lib/apt/lists/*
10 |
11 | # Install/Setup osctrl
12 | RUN useradd -ms /usr/sbin/nologin osctrl-${COMPONENT}
13 | RUN mkdir -p /opt/osctrl/bin && \
14 | mkdir -p /opt/osctrl/config && \
15 | mkdir -p /opt/osctrl/script && \
16 | chown osctrl-${COMPONENT}:osctrl-${COMPONENT} -R /opt/osctrl
17 | COPY osctrl-${COMPONENT}-${GOOS}-${GOARCH}.bin /opt/osctrl/bin/osctrl-${COMPONENT}
18 | RUN chmod 755 /opt/osctrl/bin/osctrl-${COMPONENT}
19 | USER osctrl-${COMPONENT}
20 | WORKDIR /opt/osctrl
21 | EXPOSE 9000/tcp
22 | CMD ["/opt/osctrl/bin/osctrl-tls"]
23 |
--------------------------------------------------------------------------------
/cmd/admin/static/img/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/cmd/tls/handlers/get.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/jmpsec/osctrl/pkg/utils"
7 | )
8 |
9 | // RootHandler to be used as health check
10 | func (h *HandlersTLS) RootHandler(w http.ResponseWriter, r *http.Request) {
11 | // Send response
12 | utils.HTTPResponse(w, "", http.StatusOK, []byte("💥"))
13 | }
14 |
15 | // HealthHandler for health requests
16 | func (h *HandlersTLS) HealthHandler(w http.ResponseWriter, r *http.Request) {
17 | // Send response
18 | utils.HTTPResponse(w, "", http.StatusOK, []byte("✅"))
19 | }
20 |
21 | // ErrorHandler for error requests
22 | func (h *HandlersTLS) ErrorHandler(w http.ResponseWriter, r *http.Request) {
23 | // Send response
24 | utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte("uh oh..."))
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/logging/debughttp.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/config"
5 | "github.com/rs/zerolog"
6 | lumberjack "gopkg.in/natefinch/lumberjack.v2"
7 | )
8 |
9 | const (
10 | // Default time format for loggers
11 | LoggerTimeFormat string = "2006-01-02T15:04:05.999Z07:00"
12 | )
13 |
14 | // CreateDebugHTTP to initialize the debug HTTP logger
15 | func CreateDebugHTTP(cfg config.LocalLogger) (*zerolog.Logger, error) {
16 | zerolog.TimeFieldFormat = LoggerTimeFormat
17 | z := zerolog.New(&lumberjack.Logger{
18 | Filename: cfg.FilePath,
19 | MaxSize: cfg.MaxSize,
20 | MaxBackups: cfg.MaxBackups,
21 | MaxAge: cfg.MaxAge,
22 | Compress: cfg.Compress,
23 | })
24 | logger := z.With().Caller().Timestamp().Logger()
25 | return &logger, nil
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/logging/utils.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/config"
5 | )
6 |
7 | const (
8 | // NotReturned - Value not returned from agent
9 | NotReturned = "not returned"
10 | // Mismatched - Value mismatched in log entries
11 | Mismatched = "mismatched"
12 | )
13 |
14 | // Helper to check if two DB configurations are the same
15 | func sameConfigDB(loggerOne, loggerTwo config.YAMLConfigurationDB) bool {
16 | return (loggerOne.Host == loggerTwo.Host) && (loggerOne.Port == loggerTwo.Port) && (loggerOne.Name == loggerTwo.Name)
17 | }
18 |
19 | // Helper to be used preparing metadata for each decorator
20 | func metadataVerification(dst, src string) string {
21 | if src != dst {
22 | if dst == "" {
23 | return src
24 | }
25 | if src == "" {
26 | return dst
27 | }
28 | }
29 | return src
30 | }
31 |
--------------------------------------------------------------------------------
/deploy/cicd/docker/Dockerfile-osctrl-cli:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ARG COMPONENT=cli
4 | ARG GOOS=linux
5 | ARG GOARCH=amd64
6 |
7 | # Install software
8 | RUN apt-get update -y -q && \
9 | rm -rf /var/lib/apt/lists/*
10 |
11 | # Install/Setup osctrl
12 | RUN useradd -ms /usr/sbin/nologin osctrl-${COMPONENT}
13 | RUN mkdir -p /opt/osctrl/bin && \
14 | mkdir -p /opt/osctrl/config && \
15 | mkdir -p /opt/osctrl/script && \
16 | chown osctrl-${COMPONENT}:osctrl-${COMPONENT} -R /opt/osctrl
17 |
18 | COPY osctrl-${COMPONENT}-${GOOS}-${GOARCH}.bin /opt/osctrl/bin/osctrl-${COMPONENT}
19 | RUN chmod 755 /opt/osctrl/bin/osctrl-${COMPONENT}
20 |
21 |
22 | COPY deploy/docker/conf/cli/entrypoint.sh /entrypoint.sh
23 | RUN chmod 755 /entrypoint.sh
24 |
25 | USER osctrl-${COMPONENT}
26 | WORKDIR /opt/osctrl
27 | ENTRYPOINT [ "/entrypoint.sh" ]
28 |
--------------------------------------------------------------------------------
/cmd/cli/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/jmpsec/osctrl/pkg/nodes"
7 | "github.com/jmpsec/osctrl/pkg/utils"
8 | )
9 |
10 | // Helper to convert boolean to string
11 | func stringifyBool(b bool) string {
12 | if b {
13 | return "True"
14 | }
15 | return "False"
16 | }
17 |
18 | // Helper to get what is the last seen time for a node
19 | func nodeLastSeen(n nodes.OsqueryNode) string {
20 | return utils.PastFutureTimes(n.LastSeen)
21 | }
22 |
23 | // Helper to prepare the header for output
24 | func stringSliceToAnySlice(header []string) []any {
25 | result := make([]any, len(header))
26 | for i, v := range header {
27 | result[i] = v
28 | }
29 | return result
30 | }
31 |
32 | // Helper to get the shell username
33 | func getShellUsername() string {
34 | user := os.Getenv("USER")
35 | if user == "" {
36 | user = os.Getenv("USERNAME")
37 | }
38 | if user == "" {
39 | user = "cli-user"
40 | } else {
41 | user = "env: " + user
42 | }
43 | return user
44 | }
45 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v5.0.0
5 | hooks:
6 | - id: check-ast
7 | - id: check-json
8 | - id: check-merge-conflict
9 | - id: check-symlinks
10 | - id: check-toml
11 | - id: check-xml
12 | - id: detect-aws-credentials
13 | - id: detect-private-key
14 | - id: check-yaml
15 | - id: end-of-file-fixer
16 | - id: trailing-whitespace
17 | - id: check-added-large-files
18 | args: [--maxkb=800]
19 | - id: check-docstring-first
20 | - id: requirements-txt-fixer
21 |
22 |
23 | - repo: https://github.com/Bahjat/pre-commit-golang
24 | rev: v1.0.5
25 | hooks:
26 | - id: go-fmt-import
27 | - id: go-static-check # install https://staticcheck.io/docs/
28 | - id: golangci-lint # requires github.com/golangci/golangci-lint
29 | args: [--config=.golangci.yml, --allow-parallel-runners] # optional
30 | - id: go-unit-tests
31 |
--------------------------------------------------------------------------------
/deploy/redis/redis.conf:
--------------------------------------------------------------------------------
1 | bind 127.0.0.1
2 | daemonize no
3 | supervised systemd
4 | requirepass REDIS_PASSWORD
5 |
6 | # This limit will depend on the memory available in your system
7 | maxmemory 1G
8 |
9 | # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
10 | # is reached. You can select one from the following behaviors:
11 | #
12 | # volatile-lru -> Evict using approximated LRU, only keys with an expire set.
13 | # allkeys-lru -> Evict any key using approximated LRU.
14 | # volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
15 | # allkeys-lfu -> Evict any key using approximated LFU.
16 | # volatile-random -> Remove a random key having an expire set.
17 | # allkeys-random -> Remove a random key, any key.
18 | # volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
19 | # noeviction -> Don't evict anything, just return an error on write operations.
20 | # LRU means Least Recently Used
21 | # LFU means Least Frequently Used
22 | maxmemory-policy allkeys-lfu
23 |
--------------------------------------------------------------------------------
/tools/update-modules.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Helper to update all go.mod files with the latest commit of osctrl
4 |
5 | # If more than one parameter, show usage
6 | if [ $# -gt 1 ] ; then
7 | echo "Usage: $0 "
8 | exit 1
9 | fi
10 |
11 | declare -a MODULES=()
12 |
13 | # If one parameter, that is the module directory
14 | if [ $# -eq 1 ] ; then
15 | echo "[+] Using module $1"
16 | MODULES+=( "$1" )
17 | fi
18 |
19 | # If no parameters, recursively find all go.mod files
20 | if [ $# -eq 0 ] ; then
21 | echo "[+] Finding all go.mod files..."
22 | MODULES+=( $(find . -name "go.mod" | sed 's/\/go.mod//g' | grep -v "\.$") )
23 | echo "[+] Found ${#MODULES[@]} modules"
24 | fi
25 |
26 | # Iterate over all modules
27 | for module in "${MODULES[@]}"
28 | do
29 | echo "[+] Updating module $module"
30 | cd "$module" || exit 1
31 | go get -u
32 | cd - || exit 1
33 | done
34 |
35 | # Run go mod tidy
36 | echo "[+] Running go mod tidy"
37 | go mod tidy
38 |
39 | echo "[+] Done"
40 | exit 0
41 |
--------------------------------------------------------------------------------
/cmd/cli/api-login.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "path"
8 |
9 | "github.com/jmpsec/osctrl/pkg/types"
10 | )
11 |
12 | // PostLogin to login into API to retrieve a token
13 | func (api *OsctrlAPI) PostLogin(env, username, password string, expHours int) (types.ApiLoginResponse, error) {
14 | var res types.ApiLoginResponse
15 | l := types.ApiLoginRequest{
16 | Username: username,
17 | Password: password,
18 | ExpHours: expHours,
19 | }
20 | jsonMessage, err := json.Marshal(l)
21 | if err != nil {
22 | return res, fmt.Errorf("error marshaling data %w", err)
23 | }
24 | jsonParam := bytes.NewReader(jsonMessage)
25 | reqURL := fmt.Sprintf("%s%s", api.Configuration.URL, path.Join(APIPath, APILogin, env))
26 | rawRes, err := api.PostGeneric(reqURL, jsonParam)
27 | if err != nil {
28 | return res, fmt.Errorf("error api request - %w - %s", err, string(rawRes))
29 | }
30 | if err := json.Unmarshal(rawRes, &res); err != nil {
31 | return res, fmt.Errorf("can not parse body - %w", err)
32 | }
33 | return res, nil
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/carves/db.go:
--------------------------------------------------------------------------------
1 | package carves
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // CarvedFile to keep track of carved files from nodes
10 | type CarvedFile struct {
11 | gorm.Model
12 | CarveID string `gorm:"unique;index"`
13 | RequestID string
14 | SessionID string
15 | QueryName string
16 | UUID string `gorm:"index"`
17 | NodeID uint
18 | Environment string
19 | Path string
20 | CarveSize int
21 | BlockSize int
22 | TotalBlocks int
23 | CompletedBlocks int
24 | Status string
25 | CompletedAt time.Time
26 | Carver string
27 | Archived bool
28 | ArchivePath string
29 | EnvironmentID uint
30 | }
31 |
32 | // CarvedBlock to store each block from a carve
33 | type CarvedBlock struct {
34 | gorm.Model
35 | RequestID string `gorm:"index"`
36 | SessionID string `gorm:"index"`
37 | Environment string
38 | BlockID int
39 | Data string
40 | Size int
41 | Carver string
42 | EnvironmentID uint
43 | }
44 |
--------------------------------------------------------------------------------
/deploy/cicd/docker/Dockerfile-osctrl-admin:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ARG COMPONENT=admin
4 | ARG GOOS=linux
5 | ARG GOARCH=amd64
6 |
7 | # Install software
8 | RUN apt-get update -y -q && \
9 | rm -rf /var/lib/apt/lists/*
10 |
11 | # Install/Setup osctrl
12 | RUN useradd -ms /usr/sbin/nologin osctrl-${COMPONENT}
13 | RUN mkdir -p /opt/osctrl/bin && \
14 | mkdir -p /opt/osctrl/config && \
15 | mkdir -p /opt/osctrl/script && \
16 | mkdir -p /opt/osctrl/tmpl_admin/components && \
17 | mkdir -p /opt/osctrl/static && \
18 | mkdir -p /opt/osctrl/data && \
19 | chown osctrl-${COMPONENT}:osctrl-${COMPONENT} -R /opt/osctrl
20 | COPY osctrl-${COMPONENT}-${GOOS}-${GOARCH}.bin /opt/osctrl/bin/osctrl-${COMPONENT}
21 | RUN chmod 755 /opt/osctrl/bin/osctrl-${COMPONENT}
22 |
23 | ### Copy osctrl-admin web templates ###
24 | USER osctrl-${COMPONENT}
25 | COPY cmd/admin/templates/ /opt/osctrl/tmpl_admin
26 | COPY cmd/admin/static/ /opt/osctrl/static
27 | COPY deploy/osquery/data/*.json /opt/osctrl/data/
28 |
29 | WORKDIR /opt/osctrl
30 | EXPOSE 9001/tcp
31 | CMD ["/opt/osctrl/bin/osctrl-admin"]
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS generated files
2 | .DS_Store
3 | .DS_Store?
4 | ._*
5 | .Spotlight-V100
6 | .Trashes
7 |
8 | # IDE metadata
9 | .vscode
10 | .idea
11 |
12 | # Logs
13 | *.log
14 |
15 | # Golang
16 | vendor
17 | glide.lock
18 |
19 | # Directories to ignore
20 | bin
21 | build
22 | !.github/actions/build
23 | vendor
24 | dist
25 | ignore
26 |
27 | # Plugins
28 | *.so
29 |
30 | # Deployment related files
31 | osctrl.sh
32 |
33 | # Temp directory for tests
34 | tmp
35 |
36 | # Various packages
37 | *.deb
38 | *.pkg
39 | *.rpm
40 |
41 | # Certificates
42 | certs
43 | osctrl.csr
44 | osctrl.pem
45 | osctrl.key
46 | osctrl.crt
47 | osctrl.secret
48 | dhparam.pem
49 |
50 | # Environment file for docker
51 | .env
52 |
53 | # Customization
54 | #brand.png
55 |
56 | # Random notes for debug
57 | notes.txt
58 |
59 | # Generated bins
60 | *.bin
61 |
62 | # Tests
63 | coverage.out
64 |
65 | # Configuration
66 | osctrl-api.json
67 |
68 | # Go Workspace
69 | go.work
70 | go.work.sum
71 |
72 | deploy/docker/conf/tls/*
73 | .env
74 | !deploy/docker/conf/tls/openssl.cnf.example
75 | tls.env
76 |
77 | # bruno
78 | tools/bruno/collection.bru
79 |
--------------------------------------------------------------------------------
/pkg/environments/decorators.go:
--------------------------------------------------------------------------------
1 | package environments
2 |
3 | const (
4 | // DecoratorUsers to append osquery user as result decorator
5 | DecoratorUsers = "SELECT username AS osquery_user FROM users WHERE uid = (SELECT uid FROM processes WHERE pid = (SELECT pid FROM osquery_info) LIMIT 1);"
6 | // DecoratorHostname to append hostnames as result decorator
7 | DecoratorHostname = "SELECT hostname, local_hostname FROM system_info;"
8 | // DecoratorLoggedInUser to append the first logged in user as result decorator
9 | DecoratorLoggedInUser = "SELECT user || ' (' || tty || ')' AS username FROM logged_in_users WHERE type = 'user' ORDER BY time LIMIT 1;"
10 | // DecoratorOsqueryVersionHash to append the osquery version and the configuration hash as result decorator
11 | DecoratorOsqueryVersionHash = "SELECT version AS osquery_version, config_hash FROM osquery_info WHERE config_valid = 1;"
12 | // DecoratorMD5Process to append the MD5 of the running osquery binary as result decorator
13 | DecoratorMD5Process = "SELECT md5 AS osquery_md5 FROM hash WHERE path = (SELECT path FROM processes WHERE pid = (SELECT pid FROM osquery_info));"
14 | )
15 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-dev-admin:
--------------------------------------------------------------------------------
1 | ######################################## osctrl-dev-base ########################################
2 | ARG GOLANG_VERSION=${GOLANG_VERSION:-1.25.4}
3 | FROM golang:${GOLANG_VERSION} AS osctrl-admin-dev
4 |
5 | WORKDIR /usr/src/app
6 |
7 | ARG OSQUERY_VERSION
8 | ENV GO111MODULE="on"
9 | ENV GOOS="linux"
10 | ENV CGO_ENABLED=0
11 |
12 | # Hot reloading mod
13 | RUN go install github.com/cosmtrek/air@v1.49.0
14 | RUN go install github.com/go-delve/delve/cmd/dlv@v1.22.1
15 |
16 | # Copy code
17 | COPY . /usr/src/app
18 |
19 | # Download deps
20 | RUN go mod download
21 | RUN go mod verify
22 |
23 | ### Copy osctrl-admin bin and configs ###
24 | RUN mkdir -p /opt/osctrl/bin
25 | RUN mkdir -p /opt/osctrl/config
26 | RUN mkdir -p /opt/osctrl/carved_files
27 |
28 | ### Copy osctrl-admin web templates ###
29 | COPY cmd/admin/templates/ /opt/osctrl/tmpl_admin
30 | COPY cmd/admin/static/ /opt/osctrl/static
31 | COPY deploy/osquery/data/${OSQUERY_VERSION}.json /opt/osctrl/data/${OSQUERY_VERSION}.json
32 |
33 | EXPOSE 9001
34 | ENTRYPOINT ["air", "-c", "deploy/docker/conf/dev/air/.air-osctrl-admin.toml"]
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Javier Marcos de Prado
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 |
--------------------------------------------------------------------------------
/cmd/admin/handlers/json-tags.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/jmpsec/osctrl/cmd/admin/sessions"
8 | "github.com/jmpsec/osctrl/pkg/users"
9 | "github.com/jmpsec/osctrl/pkg/utils"
10 | "github.com/rs/zerolog/log"
11 | )
12 |
13 | // JSONTagsHandler for platform/environment stats in JSON
14 | func (h *HandlersAdmin) JSONTagsHandler(w http.ResponseWriter, r *http.Request) {
15 | if h.DebugHTTPConfig.EnableHTTP {
16 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
17 | }
18 | // Get context data
19 | ctx := r.Context().Value(sessions.ContextKey(sessions.CtxSession)).(sessions.ContextValue)
20 | // Check permissions
21 | if !h.Users.CheckPermissions(ctx[sessions.CtxUser], users.AdminLevel, users.NoEnvironment) {
22 | adminErrorResponse(w, fmt.Sprintf("%s has insufficient permissions", ctx[sessions.CtxUser]), http.StatusForbidden, nil)
23 | return
24 | }
25 | // Get tags
26 | tags, err := h.Tags.All()
27 | if err != nil {
28 | log.Err(err).Msg("error getting tags")
29 | return
30 | }
31 | // Serve JSON
32 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, tags)
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/utils/string-utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGenRandomString(t *testing.T) {
10 | generatedLen := 10
11 | generated := GenRandomString(generatedLen)
12 | assert.NotEmpty(t, generated)
13 | assert.Equal(t, generatedLen, len(generated))
14 | }
15 |
16 | func TestGenKSUID(t *testing.T) {
17 | assert.NotEmpty(t, GenKSUID())
18 | }
19 |
20 | func TestGenUUID(t *testing.T) {
21 | assert.NotEmpty(t, GenUUID())
22 | }
23 |
24 | func TestCheckUUID(t *testing.T) {
25 | assert.True(t, CheckUUID(GenUUID()))
26 | }
27 |
28 | func TestStringToInteger(t *testing.T) {
29 | assert.Equal(t, int64(123), StringToInteger("123"))
30 | }
31 |
32 | func TestStringToIntegerError(t *testing.T) {
33 | assert.Equal(t, int64(0), StringToInteger("aaa"))
34 | }
35 |
36 | func TestStringToBooleanYes(t *testing.T) {
37 | assert.Equal(t, true, StringToBoolean("yes"))
38 | }
39 |
40 | func TestStringToBooleanTrue(t *testing.T) {
41 | assert.Equal(t, true, StringToBoolean("true"))
42 | }
43 |
44 | func TestStringToBooleanWhatever(t *testing.T) {
45 | assert.Equal(t, false, StringToBoolean("whatever"))
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/auditlog/utils_test.go:
--------------------------------------------------------------------------------
1 | package auditlog
2 |
3 | import "testing"
4 |
5 | func TestLogTypeToString(t *testing.T) {
6 | tests := []struct {
7 | input uint
8 | expected string
9 | }{
10 | {1, "Login"},
11 | {2, "Logout"},
12 | {3, "Node"},
13 | {4, "Query"},
14 | {5, "Carve"},
15 | {6, "Tag"},
16 | {7, "Environment"},
17 | {8, "Setting"},
18 | {9, "Visit"},
19 | {10, "User"},
20 | {0, "Unknown"},
21 | {99, "Unknown"},
22 | }
23 | al := AuditLogManager{}
24 | for _, tt := range tests {
25 | result := al.LogTypeToString(tt.input)
26 | if result != tt.expected {
27 | t.Errorf("LogTypeToString(%d) = %q; want %q", tt.input, result, tt.expected)
28 | }
29 | }
30 | }
31 |
32 | func TestSeverityToString(t *testing.T) {
33 | tests := []struct {
34 | input uint
35 | expected string
36 | }{
37 | {1, "Info"},
38 | {2, "Warning"},
39 | {3, "Error"},
40 | {0, "Unknown"},
41 | {99, "Unknown"},
42 | }
43 | al := AuditLogManager{}
44 | for _, tt := range tests {
45 | result := al.SeverityToString(tt.input)
46 | if result != tt.expected {
47 | t.Errorf("SeverityToString(%d) = %q; want %q", tt.input, result, tt.expected)
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/stats.js:
--------------------------------------------------------------------------------
1 | function statsRefresh(_target, _identifier) {
2 | $.ajax({
3 | url: '/json/stats/' + _target + '/' + _identifier,
4 | dataType: 'json',
5 | type: 'GET',
6 | contentType: 'application/json',
7 | success: function (data, textStatus, jQxhr) {
8 | $('.stats-' + _target + '-' + _identifier + '-active').text(data.active);
9 | $('.stats-' + _target + '-' + _identifier + '-inactive').text(data.inactive);
10 | $('.stats-' + _target + '-' + _identifier + '-total').text(data.total);
11 | },
12 | error: function (jqXhr, textStatus, errorThrown) {
13 | var _clientmsg = 'Client: ' + errorThrown;
14 | var _serverJSON = $.parseJSON(jqXhr.responseText);
15 | var _servermsg = 'Server: ' + _serverJSON.message;
16 | console.log('Error getting stats...');
17 | console.log(_clientmsg);
18 | console.log(_servermsg);
19 | }
20 | });
21 | }
22 |
23 | function beginStats() {
24 | var _stats = ['environment'];
25 | for (var i = 0; i < _stats.length; i++) {
26 | $('input[type="hidden"].stats-' + _stats[i] + '-value').each(function () {
27 | statsRefresh(_stats[i], $(this).val());
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/tls/handlers/get_test.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestRootHandler(t *testing.T) {
12 | req, _ := http.NewRequest("GET", "/", nil)
13 | h := CreateHandlersTLS()
14 | rr := httptest.NewRecorder()
15 | handler := http.HandlerFunc(h.RootHandler)
16 | handler.ServeHTTP(rr, req)
17 | assert.Equal(t, http.StatusOK, rr.Code)
18 | assert.Equal(t, "💥", rr.Body.String())
19 | }
20 |
21 | func TestHealthHandler(t *testing.T) {
22 | req, _ := http.NewRequest("GET", "/health", nil)
23 | h := CreateHandlersTLS()
24 | rr := httptest.NewRecorder()
25 | handler := http.HandlerFunc(h.HealthHandler)
26 | handler.ServeHTTP(rr, req)
27 | assert.Equal(t, http.StatusOK, rr.Code)
28 | assert.Equal(t, "✅", rr.Body.String())
29 | }
30 |
31 | func TestErrorHandler(t *testing.T) {
32 | req, _ := http.NewRequest("GET", "/error", nil)
33 | h := CreateHandlersTLS()
34 | rr := httptest.NewRecorder()
35 | handler := http.HandlerFunc(h.ErrorHandler)
36 | handler.ServeHTTP(rr, req)
37 | assert.Equal(t, http.StatusInternalServerError, rr.Code)
38 | assert.Equal(t, "uh oh...", rr.Body.String())
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/test-release.yml:
--------------------------------------------------------------------------------
1 | name: Test a new release with GoReleaser
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | permissions:
12 | contents: read
13 |
14 | env:
15 | GOLANG_VERSION: 1.25.4
16 |
17 | jobs:
18 | test-build:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Set up Go
27 | uses: actions/setup-go@8e57b58e57be52ac95949151e2777ffda8501267 # v5.5.0
28 | with:
29 | go-version: ${{ env.GOLANG_VERSION }}
30 |
31 | - name: Run GoReleaser build
32 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
33 | with:
34 | distribution: goreleaser
35 | version: latest
36 | args: build --snapshot --clean --single-target
37 |
38 | - name: Upload build artifacts
39 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
40 | with:
41 | name: osctrl-binaries
42 | path: dist/
43 | retention-days: 1
44 |
--------------------------------------------------------------------------------
/pkg/users/utils_test.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestSameAccessTrue(t *testing.T) {
10 | acc1 := EnvAccess{
11 | User: true,
12 | Query: true,
13 | Carve: false,
14 | Admin: false,
15 | }
16 | acc2 := EnvAccess{
17 | User: true,
18 | Query: true,
19 | Carve: false,
20 | Admin: false,
21 | }
22 | assert.Equal(t, true, SameAccess(acc1, acc2))
23 | }
24 |
25 | func TestSameAccessFalse(t *testing.T) {
26 | acc1 := EnvAccess{
27 | User: true,
28 | Query: true,
29 | Carve: false,
30 | Admin: false,
31 | }
32 | acc2 := EnvAccess{
33 | User: true,
34 | Query: false,
35 | Carve: false,
36 | Admin: false,
37 | }
38 | assert.Equal(t, false, SameAccess(acc1, acc2))
39 | }
40 |
41 | func TestGenEnvAccessAdmin(t *testing.T) {
42 | acc := EnvAccess{
43 | User: true,
44 | Query: true,
45 | Carve: true,
46 | Admin: true,
47 | }
48 | assert.Equal(t, acc, GenEnvAccess(true, false, false, false))
49 | }
50 |
51 | func TestGenEnvAccessQueryUser(t *testing.T) {
52 | acc := EnvAccess{
53 | User: true,
54 | Query: true,
55 | Carve: false,
56 | Admin: false,
57 | }
58 | assert.Equal(t, acc, GenEnvAccess(false, false, true, true))
59 | }
60 |
--------------------------------------------------------------------------------
/deploy/docker/conf/dev/air/.air-osctrl-cli.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format for osctrl-cli
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root
5 | root = "."
6 | tmp_dir = "/tmp"
7 |
8 | [build]
9 | bin = "./bin/osctrl-cli"
10 | cmd = "go build -ldflags \"-s -w -X main.buildCommit=$(git rev-parse HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)\" -o /opt/osctrl/bin/osctrl-cli /usr/src/app/cmd/cli/*.go"
11 | # It's not necessary to trigger build each time file changes if it's too frequent.
12 | delay = 1000
13 | exclude_dir = ["tmp", "vendor", "testdata", "deploy", "admin", "tls", "api"]
14 | exclude_file = []
15 | exclude_regex = ["_test\\.go"]
16 | exclude_unchanged = false
17 | follow_symlink = false
18 | full_bin = "/bin/bash"
19 | include_dir = []
20 | include_ext = ["go"]
21 | kill_delay = "0s"
22 | log = "build-errors.log"
23 | send_interrupt = false
24 | stop_on_error = true
25 |
26 | [color]
27 | app = ""
28 | build = "yellow"
29 | main = "magenta"
30 | runner = "green"
31 | watcher = "cyan"
32 |
33 | [log]
34 | time = true
35 |
36 | [misc]
37 | clean_on_exit = false
38 |
39 | [screen]
40 | clear_on_rebuild = false
41 |
--------------------------------------------------------------------------------
/deploy/docker/conf/dev/air/.air-osctrl-api.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format for osctrl-api
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root
5 | root = "."
6 | tmp_dir = "/tmp"
7 |
8 | [build]
9 | bin = "./bin/osctrl-api"
10 | cmd = "go build -ldflags \"-s -w -X main.buildCommit=$(git rev-parse HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)\" -o /opt/osctrl/bin/osctrl-api /usr/src/app/cmd/api/*.go"
11 | # It's not necessary to trigger build each time file changes if it's too frequent.
12 | delay = 1000
13 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "deploy", "admin", "tls", "cli"]
14 | exclude_file = []
15 | exclude_regex = ["_test.go"]
16 | exclude_unchanged = false
17 | follow_symlink = false
18 | full_bin = "cd /opt/osctrl && ./bin/osctrl-api"
19 | include_dir = []
20 | include_ext = ["go"]
21 | kill_delay = "0s"
22 | log = "build-errors.log"
23 | send_interrupt = false
24 | stop_on_error = true
25 |
26 | [color]
27 | app = ""
28 | build = "yellow"
29 | main = "magenta"
30 | runner = "green"
31 | watcher = "cyan"
32 |
33 | [log]
34 | time = true
35 |
36 | [misc]
37 | clean_on_exit = false
38 |
39 | [screen]
40 | clear_on_rebuild = false
41 |
--------------------------------------------------------------------------------
/deploy/docker/conf/dev/air/.air-osctrl-tls.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format for osctrl-tls
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root
5 | root = "."
6 | tmp_dir = "/tmp"
7 |
8 | [build]
9 | bin = "./bin/osctrl-tls"
10 | cmd = "go build -ldflags \"-s -w -X main.buildCommit=$(git rev-parse HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)\" -o /opt/osctrl/bin/osctrl-tls /usr/src/app/cmd/tls/*.go"
11 | # It's not necessary to trigger build each time file changes if it's too frequent.
12 | delay = 1000
13 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "deploy", "admin", "api", "cli"]
14 | exclude_file = []
15 | exclude_regex = ["_test.go"]
16 | exclude_unchanged = false
17 | follow_symlink = false
18 | full_bin = "cd /opt/osctrl && ./bin/osctrl-tls"
19 | include_dir = []
20 | include_ext = ["go"]
21 | kill_delay = "0s"
22 | log = "build-errors.log"
23 | send_interrupt = false
24 | stop_on_error = true
25 |
26 | [color]
27 | app = ""
28 | build = "yellow"
29 | main = "magenta"
30 | runner = "green"
31 | watcher = "cyan"
32 |
33 | [log]
34 | time = true
35 |
36 | [misc]
37 | clean_on_exit = false
38 |
39 | [screen]
40 | clear_on_rebuild = false
41 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: Go linting on PRs pushed to osctrl
2 |
3 | on:
4 | pull_request:
5 | branches: [main, master, develop]
6 | paths:
7 | - "**/*.go"
8 | - "go.mod"
9 | - "go.sum"
10 | - ".golangci.yml"
11 | - ".github/workflows/golangci-lint.yml"
12 |
13 | permissions:
14 | contents: read
15 |
16 | env:
17 | GOLANG_VERSION: 1.25.4
18 |
19 | jobs:
20 | golangci:
21 | name: lint
22 | runs-on: ubuntu-latest
23 | steps:
24 | ########################### Checkout code ###########################
25 | - name: Checkout code
26 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
27 |
28 | ########################### Set up Go ###############################
29 | - name: Set up Go
30 | uses: actions/setup-go@8e57b58e57be52ac95949151e2777ffda8501267 # v5.5.0
31 | with:
32 | go-version: "${{ env.GOLANG_VERSION }}"
33 | cache: true
34 |
35 | ########################### Run golangci-lint #######################
36 | - name: golangci-lint
37 | uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0
38 | with:
39 | version: latest
40 | working-directory: ./
41 | args: --timeout=5m
42 | only-new-issues: true
43 |
--------------------------------------------------------------------------------
/cmd/api/handlers/audit.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/jmpsec/osctrl/pkg/auditlog"
9 | "github.com/jmpsec/osctrl/pkg/users"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | // AuditLogsHandler - GET Handler for all audit logs
15 | func (h *HandlersApi) AuditLogsHandler(w http.ResponseWriter, r *http.Request) {
16 | // Debug HTTP if enabled
17 | if h.DebugHTTPConfig.EnableHTTP {
18 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
19 | }
20 | // Get context data and check access
21 | ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue)
22 | if !h.Users.CheckPermissions(ctx[ctxUser], users.AdminLevel, users.NoEnvironment) {
23 | apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser]))
24 | return
25 | }
26 | // Get audit logs
27 | auditLogs, err := h.AuditLog.GetAll()
28 | if err != nil {
29 | log.Err(err).Msg("error getting audit logs")
30 | return
31 | }
32 | // Serialize and serve JSON
33 | log.Debug().Msgf("Returned %d audit log entries", len(auditLogs))
34 | h.AuditLog.Visit(ctx[ctxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment)
35 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, auditLogs)
36 | }
37 |
--------------------------------------------------------------------------------
/cmd/api/handlers/utils.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/jmpsec/osctrl/pkg/logging"
7 | "github.com/jmpsec/osctrl/pkg/types"
8 | "github.com/jmpsec/osctrl/pkg/utils"
9 | "github.com/rs/zerolog/log"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // ContextValue to hold session data in the context
14 | type ContextValue map[string]string
15 |
16 | // ContextKey to help with the context key, to pass session data
17 | type ContextKey string
18 |
19 | // APIQueryData to hold query result data
20 | type APIQueryData map[string]string
21 |
22 | const (
23 | // Key to identify request context
24 | contextAPI string = "osctrl-api-context"
25 | ctxUser string = "user"
26 | )
27 |
28 | // Function to retrieve the query log by name
29 | func postgresQueryLogs(db *gorm.DB, name string) (APIQueryData, error) {
30 | var logs []logging.OsqueryQueryData
31 | data := make(APIQueryData)
32 | if err := db.Where("name = ?", name).Find(&logs).Error; err != nil {
33 | return data, err
34 | }
35 | for _, l := range logs {
36 | data[l.UUID] = l.Data
37 | }
38 | return data, nil
39 | }
40 |
41 | // Helper to handle API error responses
42 | func apiErrorResponse(w http.ResponseWriter, msg string, code int, err error) {
43 | log.Debug().Msgf("apiErrorResponse %s: %v", msg, err)
44 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, code, types.ApiErrorResponse{Error: msg})
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/logging/dispatch.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/jmpsec/osctrl/pkg/nodes"
7 | "github.com/jmpsec/osctrl/pkg/types"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | // DispatchLogs - Helper to dispatch logs
12 | func (l *LoggerTLS) DispatchLogs(data []byte, uuid, logType, environment string, metadata nodes.NodeMetadata, debug bool) {
13 | // Use metadata to update record
14 | if err := l.Nodes.UpdateMetadataByUUID(uuid, metadata); err != nil {
15 | log.Err(err).Msg("error updating metadata")
16 | }
17 | // Send data to storage
18 | // FIXME allow multiple types of logging
19 | if debug {
20 | log.Debug().Msgf("dispatching logs to %s", l.Logging)
21 | }
22 | l.Log(logType, data, environment, uuid, debug)
23 | }
24 |
25 | // DispatchQueries - Helper to dispatch queries
26 | func (l *LoggerTLS) DispatchQueries(queryData types.QueryWriteData, node nodes.OsqueryNode, debug bool) {
27 | // Prepare data to send
28 | data, err := json.Marshal(queryData)
29 | if err != nil {
30 | log.Err(err).Msg("error preparing data")
31 | }
32 | // Send data to storage
33 | // FIXME allow multiple types of logging
34 | if debug {
35 | log.Debug().Msgf("dispatching queries to %s", l.Logging)
36 | }
37 | l.QueryLog(
38 | types.QueryLog,
39 | data,
40 | node.Environment,
41 | node.UUID,
42 | queryData.Name,
43 | queryData.Status,
44 | debug)
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 |
6 | redis "github.com/go-redis/redis/v8"
7 | "github.com/jmpsec/osctrl/pkg/config"
8 | )
9 |
10 | const (
11 | // RedisKey to identify the configuration JSON key
12 | RedisKey = "redis"
13 | )
14 |
15 | // RedisManager have access to cached data
16 | type RedisManager struct {
17 | Config *config.YAMLConfigurationRedis
18 | Client *redis.Client
19 | }
20 |
21 | // GetRedis to get redis client ready
22 | func (rm *RedisManager) GetRedis() *redis.Client {
23 | opt, err := redis.ParseURL(rm.Config.ConnectionString)
24 | if err != nil {
25 | // use current behavior
26 | return redis.NewClient(&redis.Options{
27 | Addr: PrepareAddr(*rm.Config),
28 | Password: rm.Config.Password,
29 | DB: rm.Config.DB,
30 | })
31 | }
32 | return redis.NewClient(opt)
33 | }
34 |
35 | // Check to verify if connection is open and ready
36 | func (rm *RedisManager) Check() error {
37 | ctx := context.TODO()
38 | if err := rm.Client.Ping(ctx).Err(); err != nil {
39 | return err
40 | }
41 | return nil
42 | }
43 |
44 | // CreateRedisManager to initialize the redis manager struct
45 | func CreateRedisManager(cfg config.YAMLConfigurationRedis) (*RedisManager, error) {
46 | rm := &RedisManager{}
47 | rm.Config = &cfg
48 | rm.Client = rm.GetRedis()
49 | if err := rm.Check(); err != nil {
50 | return nil, err
51 | }
52 | return rm, nil
53 | }
54 |
--------------------------------------------------------------------------------
/deploy/cicd/deb/post-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | ################### Reload osctrl-{{ OSCTRL_COMPONENT }} service ###################
6 | systemctl daemon-reload
7 | echo "osctrl-{{ OSCTRL_COMPONENT }} service daemon reloaded successfully."
8 |
9 | ################### Enable osctrl-{{ OSCTRL_COMPONENT }} service ###################
10 | systemctl enable osctrl-{{ OSCTRL_COMPONENT }}.service
11 | echo "osctrl-{{ OSCTRL_COMPONENT }} service enabled successfully."
12 |
13 | ################### Start osctrl-{{ OSCTRL_COMPONENT }} service ###################
14 | systemctl start osctrl-{{ OSCTRL_COMPONENT }}.service
15 | echo "osctrl-{{ OSCTRL_COMPONENT }} service started successfully."
16 |
17 | ################### Check osctrl-{{ OSCTRL_COMPONENT }} service status ###################
18 | systemctl status osctrl-{{ OSCTRL_COMPONENT }}.service --no-pager
19 | echo "osctrl-{{ OSCTRL_COMPONENT }} service status checked successfully."
20 |
21 | ################### Print osctrl-{{ OSCTRL_COMPONENT }} service logs ###################
22 | journalctl -u osctrl-{{ OSCTRL_COMPONENT }}.service --no-pager --since "10 minutes ago"
23 | echo "osctrl-{{ OSCTRL_COMPONENT }} service logs printed successfully."
24 |
25 | ################### Print osctrl-{{ OSCTRL_COMPONENT }} version ###################
26 | /opt/osctrl/bin/osctrl-{{ OSCTRL_COMPONENT }} --version
27 | echo "osctrl-{{ OSCTRL_COMPONENT }} version printed successfully."
28 |
--------------------------------------------------------------------------------
/deploy/docker/conf/dev/air/.air-osctrl-admin.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format for osctrl-admin
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root
5 | root = "."
6 | tmp_dir = "/tmp"
7 |
8 | [build]
9 | bin = "./bin/osctrl-admin"
10 | pre_cmd = ["cp -R /usr/src/app/cmd/admin/templates/ /opt/osctrl/tmpl_admin", "cp -R /usr/src/app/cmd/admin/static/ /opt/osctrl/static"]
11 | cmd = "go build -ldflags \"-s -w -X main.buildCommit=$(git rev-parse HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)\" -o /opt/osctrl/bin/osctrl-admin /usr/src/app/cmd/admin/*.go"
12 | # It's not necessary to trigger build each time file changes if it's too frequent.
13 | delay = 1000
14 | exclude_dir = ["tmp", "vendor", "testdata", "deploy", "api", "cli", "tls"]
15 | exclude_file = []
16 | exclude_regex = ["_test\\.go"]
17 | exclude_unchanged = false
18 | follow_symlink = false
19 | full_bin = "cd /opt/osctrl && ./bin/osctrl-admin"
20 | include_dir = []
21 | include_ext = ["go"]
22 | kill_delay = "0s"
23 | log = "build-errors.log"
24 | send_interrupt = false
25 | stop_on_error = true
26 |
27 | [color]
28 | app = ""
29 | build = "yellow"
30 | main = "magenta"
31 | runner = "green"
32 | watcher = "cyan"
33 |
34 | [log]
35 | time = true
36 |
37 | [misc]
38 | clean_on_exit = false
39 |
40 | [screen]
41 | clear_on_rebuild = false
42 |
--------------------------------------------------------------------------------
/deploy/docker/conf/osquery/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ENV_NAME="${ENV_NAME:=dev}"
4 | HOST="${HOST:=nginx}"
5 | WAIT="${WAIT:=5}"
6 |
7 | if [ ! -f "/etc/osquery/osquery.secret" ]; then
8 | ######################################### Wait until DB is up #########################################
9 | until /opt/osctrl/bin/osctrl-cli check-db
10 | do
11 | echo "DB is not ready"
12 | sleep $WAIT
13 | done
14 |
15 | ######################################### Osquery config #########################################
16 | # Wait until for env to exist
17 | until /opt/osctrl/bin/osctrl-cli --db env show --name "${ENV_NAME}"
18 | do
19 | echo "${ENV_NAME} does not exist"
20 | sleep 3
21 | done
22 |
23 | # Get enroll secret
24 | /opt/osctrl/bin/osctrl-cli --db env node-actions --name "${ENV_NAME}" secret > /etc/osquery/osquery.secret
25 |
26 | # Get server cert
27 | echo "" | openssl s_client -connect ${HOST}:443 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > /etc/osquery/osctrl.crt
28 |
29 | # Get and set Osquery flags
30 | /opt/osctrl/bin/osctrl-cli --db env node-actions --name "${ENV_NAME}" show-flags > /etc/osquery/osquery.flags
31 | sed -i "s#__SECRET_FILE__#/etc/osquery/osquery.secret#g" /etc/osquery/osquery.flags
32 | echo "--tls_server_certs=/etc/osquery/osctrl.crt" >> /etc/osquery/osquery.flags
33 | fi
34 |
35 | # Run Osquery
36 | /opt/osquery/bin/osqueryd --flagfile=/etc/osquery/osquery.flags --verbose
37 |
--------------------------------------------------------------------------------
/pkg/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/jmpsec/osctrl/pkg/utils"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestBytesReceivedConversionBytes(t *testing.T) {
11 | assert.NotEmpty(t, utils.BytesReceivedConversion(123))
12 | assert.Equal(t, "123 bytes", utils.BytesReceivedConversion(123))
13 | }
14 |
15 | func TestBytesReceivedConversionKBytes(t *testing.T) {
16 | assert.NotEmpty(t, utils.BytesReceivedConversion(1024))
17 | assert.Equal(t, "1.0 KB", utils.BytesReceivedConversion(1024))
18 | }
19 |
20 | func TestBytesReceivedConversionMBytes(t *testing.T) {
21 | assert.NotEmpty(t, utils.BytesReceivedConversion(1048576))
22 | assert.Equal(t, "1.0 MB", utils.BytesReceivedConversion(1048576))
23 | }
24 |
25 | func TestRandomForNames(t *testing.T) {
26 | assert.NotEmpty(t, utils.RandomForNames())
27 | assert.Equal(t, 32, len(utils.RandomForNames()))
28 | }
29 |
30 | func TestIntersect(t *testing.T) {
31 | var slice1 = []uint{1, 2, 3, 4, 5}
32 | var slice2 = []uint{3, 4, 5, 6, 7}
33 | var expected = []uint{3, 4, 5}
34 | assert.Equal(t, expected, utils.Intersect(slice1, slice2))
35 | slice1 = utils.Intersect(slice1, slice2)
36 | assert.Equal(t, expected, slice1)
37 | }
38 |
39 | func TestIntersectEmpty(t *testing.T) {
40 | var slice1 = []uint{}
41 | var slice2 = []uint{3, 4, 5, 6, 7}
42 | var expected = []uint{3, 4, 5, 6, 7}
43 | assert.Equal(t, expected, utils.Intersect(slice1, slice2))
44 | }
45 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | Like any other open source projects, there are multiple ways to contribute to osctrl:
4 |
5 | * As a developer, depending on your skills and experience,
6 | * As a user who enjoys the project and wants to help.
7 |
8 | ##### Reporting Bugs
9 |
10 | If you found something broken or not working properly, feel free to create an issue in Github with as much information as possible, such as logs and how to reproduce the problem. Before opening the issue, make sure that:
11 |
12 | * You have read this documentation,
13 | * You are using the latest stable version of osctrl,
14 | * You already searched other issues to see if your problem or request was already reported.
15 |
16 | ##### Improving the Documentation
17 |
18 | You can improve this documentation by forking its repository, updating the content and sending a pull request.
19 |
20 |
21 | #### We ❤️ Pull Requests
22 |
23 | A pull request does not need to be a fix for a bug or implementing something new. Software can always be improved, legacy code removed and tests are always welcome!
24 |
25 | Please do not be afraid of contributing code, make sure it follows these rules:
26 |
27 | * Your code compiles, does not break any of the existing code in the master branch and does not cause conflicts,
28 | * The code is readable and has comments, that aren’t superfluous or unnecessary,
29 | * An overview or context is provided as body of the Pull Request. It does not need to be too extensive.
30 |
31 | Extra points if your code comes with tests!
32 |
--------------------------------------------------------------------------------
/cmd/admin/saml.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "crypto/x509"
7 | "fmt"
8 | "net/http"
9 | "net/url"
10 |
11 | "github.com/crewjam/saml"
12 | "github.com/crewjam/saml/samlsp"
13 | "github.com/jmpsec/osctrl/pkg/config"
14 | )
15 |
16 | // Structure to keep all SAML related data
17 | type samlThings struct {
18 | RootURL *url.URL
19 | IdpMetadataURL *url.URL
20 | IdpMetadata *saml.EntityDescriptor
21 | KeyPair tls.Certificate
22 | }
23 |
24 | // Function to initialize variables when using SAML for authentication
25 | func keypairSAML(config config.YAMLConfigurationSAML) (samlThings, error) {
26 | var data samlThings
27 | var err error
28 | data.KeyPair, err = tls.LoadX509KeyPair(config.CertPath, config.KeyPath)
29 | if err != nil {
30 | return data, fmt.Errorf("loadX509KeyPair %w", err)
31 | }
32 | data.KeyPair.Leaf, err = x509.ParseCertificate(data.KeyPair.Certificate[0])
33 | if err != nil {
34 | return data, fmt.Errorf("parseCertificate %w", err)
35 | }
36 | data.IdpMetadataURL, err = url.Parse(config.MetaDataURL)
37 | if err != nil {
38 | return data, fmt.Errorf("parse MetadataURL %w", err)
39 | }
40 | data.IdpMetadata, err = samlsp.FetchMetadata(context.Background(), http.DefaultClient, *data.IdpMetadataURL)
41 | if err != nil {
42 | return data, fmt.Errorf("fetch Metadata %w", err)
43 | }
44 | data.RootURL, err = url.Parse(config.RootURL)
45 | if err != nil {
46 | return data, fmt.Errorf("parse RootURL %w", err)
47 | }
48 | return data, nil
49 | }
50 |
--------------------------------------------------------------------------------
/deploy/config/tls.yaml:
--------------------------------------------------------------------------------
1 | # YAML configuration for osctrl-tls
2 |
3 | service:
4 | listener: "127.0.0.1"
5 | port: 1234
6 | # Valid values: "debug", "info", "warn", "error"
7 | logLevel: "info"
8 | # Valid values: "json", "console"
9 | logFormat: "json"
10 | host: "osctrl-tls-dev"
11 | # Valid values: "none", "json", "db", "saml", "oidc", "oauth"
12 | auth: "none"
13 |
14 | db:
15 | host: "127.0.0.1"
16 | port: 5432
17 | name: "osctrl"
18 | username: "osctrl-db-user"
19 | password: "osctrl-db-pass"
20 | sslmode: "disable"
21 | maxIdleConns: 20
22 | maxOpenConns: 100
23 | connMaxLifetime: 30
24 | connRetry: 10
25 |
26 | batchWriter:
27 | writerBatchSize: 50
28 | writerTimeout: 60
29 | writerBufferSize: 2000
30 |
31 | redis:
32 | host: "127.0.0.1"
33 | port: 6379
34 | password: "osctrl-redis-pass"
35 | connectionString: ""
36 | db: 0
37 | connRetry: 10
38 |
39 | metrics:
40 | enabled: false
41 | listener: "0.0.0.0"
42 | port: 9090
43 |
44 | osctrld:
45 | enabled: false
46 |
47 | tls:
48 | termination: false
49 | certificateFile: "/path/to/osctrl.pem"
50 | keyFile: "/path/to/osctrl.key"
51 |
52 | logger:
53 | # Valid values: "none", "stdout", "file", "db", "graylog", "splunk", "logstash", "kinesis", "s3", "kafka", "elastic"
54 | type: "db"
55 | loggerDBSame: false
56 | alwaysLog: false
57 |
58 |
59 |
60 | carver:
61 | # Valid values: "none", "local", "db", "s3"
62 | type: "db"
63 |
64 | debug:
65 | enableHttp: false
66 | httpFile: "debug-http-tls.log"
67 | showBody: false
68 |
--------------------------------------------------------------------------------
/cmd/admin/static/css/osquery.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'osquery';
3 | src: url('/static/fonts/osquery.eot?zcdbov');
4 | src: url('/static/fonts/osquery.eot?zcdbov#iefix') format('embedded-opentype'),
5 | url('/static/fonts/osquery.ttf?zcdbov') format('truetype'),
6 | url('/static/fonts/osquery.woff?zcdbov') format('woff'),
7 | url('/static/fonts/osquery.woff2?zcdbov') format('woff2'),
8 | url('/static/fonts/osquery.svg?zcdbov#osquery') format('svg');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 |
13 | [class^="icon-"], [class*=" icon-"] {
14 | /* use !important to prevent issues with browser extensions that change fonts */
15 | font-family: 'osquery' !important;
16 | speak: none;
17 | font-style: normal;
18 | font-weight: normal;
19 | font-variant: normal;
20 | text-transform: none;
21 | line-height: 1;
22 |
23 | /* Better Font Rendering =========== */
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | .icon-osquery_gray .path1:before {
29 | content: "\e903";
30 | color: rgb(0, 0, 0);
31 | }
32 | .icon-osquery_gray .path2:before {
33 | content: "\e904";
34 | margin-left: -1em;
35 | color: rgb(160, 160, 160);
36 | }
37 | .icon-osquery .path1:before {
38 | content: "\e900";
39 | color: rgb(0, 0, 0);
40 | }
41 | .icon-osquery .path2:before {
42 | content: "\e901";
43 | margin-left: -1em;
44 | color: rgb(111, 101, 171);
45 | }
46 | .icon-osquery .path3:before {
47 | content: "\e902";
48 | margin-left: -1em;
49 | color: rgb(165, 150, 255);
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/auditlog/utils.go:
--------------------------------------------------------------------------------
1 | package auditlog
2 |
3 | const (
4 | // Log type strings
5 | LogTypeLoginStr = "Login"
6 | LogTypeLogoutStr = "Logout"
7 | LogTypeNodeStr = "Node"
8 | LogTypeQueryStr = "Query"
9 | LogTypeCarveStr = "Carve"
10 | LogTypeTagStr = "Tag"
11 | LogTypeEnvStr = "Environment"
12 | LogTypeSettingStr = "Setting"
13 | LogTypeVisitStr = "Visit"
14 | LogTypeUserStr = "User"
15 | LogTypeUnknown = "Unknown"
16 | // Severity strings
17 | SeverityInfoStr = "Info"
18 | SeverityWarningStr = "Warning"
19 | SeverityErrorStr = "Error"
20 | SeverityUnknownStr = "Unknown"
21 | )
22 |
23 | // LogTypeToString to convert log type to string
24 | func (m *AuditLogManager) LogTypeToString(logType uint) string {
25 | switch logType {
26 | case 1:
27 | return LogTypeLoginStr
28 | case 2:
29 | return LogTypeLogoutStr
30 | case 3:
31 | return LogTypeNodeStr
32 | case 4:
33 | return LogTypeQueryStr
34 | case 5:
35 | return LogTypeCarveStr
36 | case 6:
37 | return LogTypeTagStr
38 | case 7:
39 | return LogTypeEnvStr
40 | case 8:
41 | return LogTypeSettingStr
42 | case 9:
43 | return LogTypeVisitStr
44 | case 10:
45 | return LogTypeUserStr
46 | default:
47 | return LogTypeUnknown
48 | }
49 | }
50 |
51 | // SeverityToString to convert severity to string
52 | func (m *AuditLogManager) SeverityToString(severity uint) string {
53 | switch severity {
54 | case 1:
55 | return SeverityInfoStr
56 | case 2:
57 | return SeverityWarningStr
58 | case 3:
59 | return SeverityErrorStr
60 | default:
61 | return SeverityUnknownStr
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/utils/string-utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | "strconv"
7 |
8 | "github.com/google/uuid"
9 | "github.com/segmentio/ksuid"
10 | )
11 |
12 | const (
13 | errorRandomString string = "SomethingRandomWentWrong"
14 | )
15 |
16 | // GenRandomString - Helper to generate a random string of n characters
17 | func GenRandomString(n int) string {
18 | b := make([]byte, n)
19 | _, err := rand.Read(b)
20 | // Note that err == nil only if we read len(b) bytes.
21 | if err != nil {
22 | return errorRandomString
23 | }
24 | return base64.URLEncoding.EncodeToString(b)[:n]
25 | }
26 |
27 | // GenKSUID - Helper to generate a KSUID
28 | // See https://github.com/segmentio/ksuid for more info about KSUIDs
29 | func GenKSUID() string {
30 | return ksuid.New().String()
31 | }
32 |
33 | // GenUUID - Helper to generate a UUID
34 | // See https://github.com/google/uuid for more info about UUIDs
35 | func GenUUID() string {
36 | return uuid.New().String()
37 | }
38 |
39 | // CheckUUID - Helper to check if a string is a valid UUID
40 | func CheckUUID(s string) bool {
41 | _, err := uuid.Parse(s)
42 | return err == nil
43 | }
44 |
45 | // StringToInteger - Helper to convert a string into integer
46 | func StringToInteger(s string) int64 {
47 | v, err := strconv.ParseInt(s, 10, 64)
48 | if err != nil {
49 | return 0
50 | }
51 | return v
52 | }
53 |
54 | // StringToBoolean - Helper to convert a string into boolean
55 | func StringToBoolean(s string) bool {
56 | if s == "yes" || s == "true" || s == "1" {
57 | return true
58 | }
59 | return false
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/settings.js:
--------------------------------------------------------------------------------
1 | function addSetting() {
2 | $("#addSettingModal").modal();
3 | }
4 |
5 | function confirmAddSetting() {
6 | var _csrftoken = $("#csrftoken").val();
7 |
8 | var _url = window.location.pathname;
9 |
10 | var _name = $("#setting_name").val();
11 | var _type = $("#setting_type").val();
12 | var _value = $("#setting_value").val();
13 |
14 | var data = {
15 | csrftoken: _csrftoken,
16 | action: 'add',
17 | name: _name,
18 | type: _type,
19 | value: _value,
20 | };
21 | sendPostRequest(data, _url, _url, false);
22 | }
23 |
24 | function confirmDeleteSetting(_name) {
25 | var modal_message = 'Are you sure you want to delete the setting ' + _name + '?';
26 | $("#confirmModalMessage").text(modal_message);
27 | $('#confirm_action').click(function () {
28 | $('#confirmModal').modal('hide');
29 | deleteSetting(_name);
30 | });
31 | $("#confirmModal").modal();
32 | }
33 |
34 | function deleteSetting(_name) {
35 | var _csrftoken = $("#csrftoken").val();
36 |
37 | var _url = window.location.pathname;
38 |
39 | var _type = $("#setting_type").val();
40 | var _value = $("#setting_value").val();
41 |
42 | var data = {
43 | csrftoken: _csrftoken,
44 | action: 'delete',
45 | name: _name,
46 | };
47 | sendPostRequest(data, _url, _url, false);
48 | }
49 |
50 | function changeBooleanSetting(_name) {
51 | var _csrftoken = $("#csrftoken").val();
52 | var _value = $("#" + _name).is(':checked');
53 |
54 | var _url = window.location.pathname;
55 |
56 | var data = {
57 | csrftoken: _csrftoken,
58 | action: 'change',
59 | name: _name,
60 | type: 'boolean',
61 | boolean: _value,
62 | };
63 | sendPostRequest(data, _url, '', false);
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/tags/utils_test.go:
--------------------------------------------------------------------------------
1 | package tags
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestRandomColor(t *testing.T) {
10 | color1 := RandomColor()
11 | color2 := RandomColor()
12 | assert.NotEqual(t, false, color1 != color2)
13 | }
14 |
15 | func TestGetHex(t *testing.T) {
16 | assert.Equal(t, "00", GetHex(0))
17 | assert.Equal(t, "0a", GetHex(10))
18 | assert.Equal(t, "ff", GetHex(255))
19 | }
20 |
21 | func TestTagTypeDecorator(t *testing.T) {
22 | assert.Equal(t, "env", TagTypeDecorator(0))
23 | assert.Equal(t, "platform", TagTypeDecorator(2))
24 | assert.Equal(t, "custom", TagTypeDecorator(4))
25 | assert.Equal(t, "unknown", TagTypeDecorator(5))
26 | }
27 |
28 | func TestTagTypeParser(t *testing.T) {
29 | assert.Equal(t, uint(0), TagTypeParser("env"))
30 | assert.Equal(t, uint(2), TagTypeParser("platform"))
31 | assert.Equal(t, uint(4), TagTypeParser("custom"))
32 | assert.Equal(t, uint(5), TagTypeParser("unknown"))
33 | }
34 |
35 | func TestTagCustom(t *testing.T) {
36 | assert.Equal(t, "env", SetCustomTag(0, "CUSTOM-VALUE"))
37 | assert.Equal(t, "platform", SetCustomTag(2, "CUSTOM-VALUE"))
38 | assert.Equal(t, "CUSTOM-VALUE", SetCustomTag(4, "CUSTOM-VALUE"))
39 | assert.Equal(t, "unknown", SetCustomTag(5, "CUSTOM-VALUE"))
40 | }
41 |
42 | func TestGetStrTagName(t *testing.T) {
43 | assert.Equal(t, "VALUE", GetStrTagName("custom:VALUE"))
44 | assert.Equal(t, "VALUE:EXTRA", GetStrTagName("custom:VALUE:EXTRA"))
45 | assert.Equal(t, "VALUE", GetStrTagName("VALUE"))
46 | }
47 |
48 | func TestValidateCustom(t *testing.T) {
49 | assert.Equal(t, "custom", ValidateCustom("custom"))
50 | assert.Equal(t, "unknown", ValidateCustom("anything"))
51 | assert.Equal(t, "unknown", ValidateCustom(""))
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/environments.js:
--------------------------------------------------------------------------------
1 | function createEnvironment() {
2 | $("#createEnvironmentModal").modal();
3 | }
4 |
5 | function confirmCreateEnvironment() {
6 | var _csrftoken = $("#csrftoken").val();
7 |
8 | var _url = window.location.pathname;
9 |
10 | var _name = $("#environment_name").val();
11 | var _type = $("#environment_type").val();
12 | var _hostname = $("#environment_host").val();
13 | var _icon = $("#environment_icon").val();
14 |
15 | var data = {
16 | csrftoken: _csrftoken,
17 | action: 'create',
18 | name: _name,
19 | type: _type,
20 | hostname: _hostname,
21 | icon: _icon,
22 | };
23 | sendPostRequest(data, _url, _url, false);
24 | }
25 |
26 | function confirmDeleteEnvironment(_env) {
27 | var modal_message = 'Are you sure you want to delete the environment ' + _env + '?';
28 | $("#confirmModalMessage").text(modal_message);
29 | $('#confirm_action').click(function () {
30 | $('#confirmModal').modal('hide');
31 | deleteEnvironment(_env);
32 | });
33 | $("#confirmModal").modal();
34 | }
35 |
36 | function deleteEnvironment(_env) {
37 | var _csrftoken = $("#csrftoken").val();
38 |
39 | var _url = window.location.pathname;
40 |
41 | var data = {
42 | csrftoken: _csrftoken,
43 | action: 'delete',
44 | name: _env,
45 | };
46 | sendPostRequest(data, _url, _url, false);
47 | }
48 |
49 | function changeDebugHTTP(_env) {
50 | var _csrftoken = $("#csrftoken").val();
51 | var _value = $("#" + _env + "_debug_check").is(':checked');
52 |
53 | var _url = window.location.pathname;
54 |
55 | var data = {
56 | csrftoken: _csrftoken,
57 | action: 'debug',
58 | debughttp: _value,
59 | name: _env,
60 | };
61 | sendPostRequest(data, _url, '', false);
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/tls/settings.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jmpsec/osctrl/pkg/config"
7 | "github.com/jmpsec/osctrl/pkg/settings"
8 | )
9 |
10 | // Function to load all settings for the service
11 | func loadingSettings(mgr *settings.Settings, cfg *config.ServiceParameters) error {
12 | // Check if service settings for accelerated seconds is ready
13 | if !mgr.IsValue(config.ServiceTLS, settings.AcceleratedSeconds, settings.NoEnvironmentID) {
14 | if err := mgr.NewIntegerValue(config.ServiceTLS, settings.AcceleratedSeconds, int64(defaultAccelerate), settings.NoEnvironmentID); err != nil {
15 | return fmt.Errorf("failed to add %s to configuration: %w", settings.AcceleratedSeconds, err)
16 | }
17 | }
18 | // Check if service settings for environments refresh is ready
19 | if !mgr.IsValue(config.ServiceTLS, settings.RefreshEnvs, settings.NoEnvironmentID) {
20 | if err := mgr.NewIntegerValue(config.ServiceTLS, settings.RefreshEnvs, int64(defaultRefresh), settings.NoEnvironmentID); err != nil {
21 | return fmt.Errorf("failed to add %s to configuration: %w", settings.RefreshEnvs, err)
22 | }
23 | }
24 | // Check if service settings for enroll/remove oneliner links is ready
25 | if !mgr.IsValue(config.ServiceTLS, settings.OnelinerExpiration, settings.NoEnvironmentID) {
26 | if err := mgr.NewBooleanValue(config.ServiceTLS, settings.OnelinerExpiration, defaultOnelinerExpiration, settings.NoEnvironmentID); err != nil {
27 | return fmt.Errorf("failed to add %s to configuration: %w", settings.OnelinerExpiration, err)
28 | }
29 | }
30 | // Write JSON config to settings
31 | if err := mgr.SetTLSJSON(cfg, settings.NoEnvironmentID); err != nil {
32 | return fmt.Errorf("failed to add JSON values to configuration: %w", err)
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/api/settings.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jmpsec/osctrl/pkg/config"
7 | "github.com/jmpsec/osctrl/pkg/settings"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | // Function to load all settings for the service
12 | func loadingSettings(mgr *settings.Settings, cfg *config.ServiceParameters) error {
13 | log.Debug().Msg("Initializing settings")
14 | // Check if service settings for metrics is ready, initialize if so
15 | if !mgr.IsValue(config.ServiceAPI, settings.ServiceMetrics, settings.NoEnvironmentID) {
16 | if err := mgr.NewBooleanValue(config.ServiceAPI, settings.ServiceMetrics, false, settings.NoEnvironmentID); err != nil {
17 | return fmt.Errorf("failed to add %s to settings: %w", settings.ServiceMetrics, err)
18 | }
19 | }
20 | // Check if service settings for environments refresh is ready
21 | if !mgr.IsValue(config.ServiceAPI, settings.RefreshEnvs, settings.NoEnvironmentID) {
22 | if err := mgr.NewIntegerValue(config.ServiceAPI, settings.RefreshEnvs, int64(defaultRefresh), settings.NoEnvironmentID); err != nil {
23 | return fmt.Errorf("failed to add %s to settings: %w", settings.RefreshEnvs, err)
24 | }
25 | }
26 | // Check if service settings for settings refresh is ready
27 | if !mgr.IsValue(config.ServiceAPI, settings.RefreshSettings, settings.NoEnvironmentID) {
28 | if err := mgr.NewIntegerValue(config.ServiceAPI, settings.RefreshSettings, int64(defaultRefresh), settings.NoEnvironmentID); err != nil {
29 | return fmt.Errorf("failed to add %s to settings: %w", settings.RefreshSettings, err)
30 | }
31 | }
32 | // Write JSON config to settings
33 | if err := mgr.SetAPIJSON(cfg, settings.NoEnvironmentID); err != nil {
34 | return fmt.Errorf("failed to add JSON values to configuration: %w", err)
35 | }
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/cmd/admin/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "os"
7 | "strings"
8 |
9 | "github.com/jmpsec/osctrl/pkg/config"
10 | "github.com/jmpsec/osctrl/pkg/types"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | // Function to load the JSON data for osquery tables
15 | func loadOsqueryTables(file string) ([]types.OsqueryTable, error) {
16 | var tables []types.OsqueryTable
17 | jsonFile, err := os.Open(file)
18 | if err != nil {
19 | return tables, err
20 | }
21 | defer func() {
22 | if err := jsonFile.Close(); err != nil {
23 | log.Fatal().Msgf("Failed to close tables file %v", err)
24 | }
25 | }()
26 | byteValue, _ := io.ReadAll(jsonFile)
27 | if err := json.Unmarshal(byteValue, &tables); err != nil {
28 | return tables, err
29 | }
30 | // Add a string for platforms to be used as filter
31 | for i, t := range tables {
32 | filter := ""
33 | for _, p := range t.Platforms {
34 | filter += " filter-" + p
35 | }
36 | tables[i].Filter = strings.TrimSpace(filter)
37 | }
38 | return tables, nil
39 | }
40 |
41 | // Helper to convert YAML settings loaded from file to settings
42 | func loadedYAMLToServiceParams(yml config.AdminConfiguration, loadedFile string) *config.ServiceParameters {
43 | return &config.ServiceParameters{
44 | ConfigFlag: true,
45 | ServiceConfigFile: loadedFile,
46 | Service: &yml.Service,
47 | DB: &yml.DB,
48 | Redis: &yml.Redis,
49 | Osquery: &yml.Osquery,
50 | Osctrld: &yml.Osctrld,
51 | SAML: &yml.SAML,
52 | JWT: &yml.JWT,
53 | TLS: &yml.TLS,
54 | Logger: &yml.Logger,
55 | Carver: &yml.Carver,
56 | Admin: &yml.Admin,
57 | Debug: &yml.Debug,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/md5"
5 | "crypto/rand"
6 | "encoding/hex"
7 | "fmt"
8 | )
9 |
10 | const bytesUnit = 1024
11 |
12 | // BytesReceivedConversion - Helper to format bytes received into KB, MB, TB... Binary format
13 | func BytesReceivedConversion(b int) string {
14 | if b < bytesUnit {
15 | return fmt.Sprintf("%d bytes", b)
16 | }
17 | div, exp := int64(bytesUnit), 0
18 | for n := b / bytesUnit; n >= bytesUnit; n /= bytesUnit {
19 | div *= bytesUnit
20 | exp++
21 | }
22 | return fmt.Sprintf("%.1f %cB",
23 | float64(b)/float64(div), "KMGTPE"[exp])
24 | }
25 |
26 | // Helper to generate a random MD5 to be used with queries/carves
27 | func RandomForNames() string {
28 | b := make([]byte, 32)
29 | _, _ = rand.Read(b)
30 | hasher := md5.New()
31 | _, _ = hasher.Write([]byte(fmt.Sprintf("%x", b)))
32 | return hex.EncodeToString(hasher.Sum(nil))
33 | }
34 |
35 | // Intersect returns the intersection of two slices of uints
36 | func Intersect(slice1, slice2 []uint) []uint {
37 | if len(slice1) == 0 {
38 | return slice2
39 | }
40 | // If slice2 is empty, return slice1
41 | if len(slice2) == 0 {
42 | return slice1
43 | }
44 | set := make(map[uint]struct{})
45 | for _, item := range slice1 {
46 | set[item] = struct{}{} // Add items from slice1 to the set
47 | }
48 | intersection := []uint{}
49 | for _, item := range slice2 {
50 | if _, exists := set[item]; exists {
51 | intersection = append(intersection, item)
52 | delete(set, item) // Ensure uniqueness in the result
53 | }
54 | }
55 | return intersection
56 | }
57 |
58 | // Contains checks if string is in the slice
59 | func Contains(all []string, target string) bool {
60 | for _, s := range all {
61 | if s == target {
62 | return true
63 | }
64 | }
65 | return false
66 | }
67 |
--------------------------------------------------------------------------------
/deploy/nginx/ssl.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen PUBLIC_PORT ssl default deferred;
3 |
4 | ssl_certificate CER_FILE;
5 | ssl_certificate_key KEY_FILE;
6 |
7 | # Improve HTTPS performance with session resumption
8 | ssl_session_cache shared:SSL:10m;
9 | ssl_session_timeout 5m;
10 |
11 | # Enable server-side protection against BEAST attacks
12 | ssl_prefer_server_ciphers on;
13 | ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
14 |
15 | # Disable SSLv3
16 | ssl_protocols TLSv1.2 TLSv1.3;
17 |
18 | # Diffie-Hellman parameter for DHE ciphersuites
19 | # $ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
20 | ssl_dhparam DHPARAM_FILE;
21 |
22 | # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox)
23 | ssl_stapling on;
24 | ssl_stapling_verify on;
25 | ssl_trusted_certificate CER_FILE;
26 | resolver 1.1.1.1 8.8.8.8 valid=300s;
27 | resolver_timeout 5s;
28 |
29 | # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security)
30 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
31 | add_header X-Frame-Options DENY;
32 |
33 | add_header Cache-Control "no-cache, no-store";
34 | add_header Pragma "no-cache";
35 | expires -1;
36 |
37 | location / {
38 | proxy_set_header Host $host;
39 | proxy_set_header X-Real-IP $remote_addr;
40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
41 | proxy_set_header X-Forwarded-Proto $scheme;
42 |
43 | # Fix the “It appears that your reverse proxy set up is broken" error.
44 | proxy_pass http://PRIVATE_HOST:PRIVATE_PORT;
45 | proxy_read_timeout 90;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/logging/stdout.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/settings"
5 | "github.com/jmpsec/osctrl/pkg/types"
6 | "github.com/rs/zerolog/log"
7 | )
8 |
9 | // LoggerStdout will be used to log data using stdout
10 | type LoggerStdout struct {
11 | Enabled bool
12 | }
13 |
14 | // CreateLoggerStdout to initialize the logger
15 | func CreateLoggerStdout() (*LoggerStdout, error) {
16 | return &LoggerStdout{
17 | Enabled: true,
18 | }, nil
19 | }
20 |
21 | // Settings - Function to prepare settings for the logger
22 | func (logStdout *LoggerStdout) Settings(mgr *settings.Settings) {
23 | log.Info().Msg("No stdout logging settings")
24 | }
25 |
26 | // Log - Function that sends JSON result/status/query logs to stdout
27 | func (logStdout *LoggerStdout) Log(logType string, data []byte, environment, uuid string, debug bool) {
28 | switch logType {
29 | case types.StatusLog:
30 | logStdout.Status(data, environment, uuid, debug)
31 | case types.ResultLog:
32 | logStdout.Result(data, environment, uuid, debug)
33 | }
34 | }
35 |
36 | // Status - Function that sends JSON status logs to stdout
37 | func (logStdout *LoggerStdout) Status(data []byte, environment, uuid string, debug bool) {
38 | log.Info().Msgf("Status: %s:%s - %d bytes [%s]", environment, uuid, len(data), string(data))
39 | }
40 |
41 | // Result - Function that sends JSON result logs to stdout
42 | func (logStdout *LoggerStdout) Result(data []byte, environment, uuid string, debug bool) {
43 | log.Info().Msgf("Result: %s:%s - %d bytes [%s]", environment, uuid, len(data), string(data))
44 | }
45 |
46 | // Query - Function that sends JSON query logs to stdout
47 | func (logStdout *LoggerStdout) Query(data []byte, environment, uuid, name string, status int, debug bool) {
48 | log.Info().Msgf("Query: %s:%d - %s:%s - %d bytes [%s]", name, status, environment, uuid, len(data), string(data))
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/cache/metrics.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "github.com/prometheus/client_golang/prometheus"
5 | )
6 |
7 | // Metric names and help text
8 | const (
9 | cacheHitsName = "osctrl_cache_hits_total"
10 | cacheHitsHelp = "Total number of cache hits"
11 | cacheMissesName = "osctrl_cache_misses_total"
12 | cacheMissesHelp = "Total number of cache misses"
13 | cacheEvictionsName = "osctrl_cache_evictions_total"
14 | cacheEvictionsHelp = "Total number of cache evictions"
15 | cacheItemsName = "osctrl_cache_items"
16 | cacheItemsHelp = "Current number of items in cache"
17 | )
18 |
19 | var (
20 | // CacheHits tracks the number of cache hits
21 | CacheHits = prometheus.NewCounterVec(
22 | prometheus.CounterOpts{
23 | Name: cacheHitsName,
24 | Help: cacheHitsHelp,
25 | },
26 | []string{"cache_name"},
27 | )
28 |
29 | // CacheMisses tracks the number of cache misses
30 | CacheMisses = prometheus.NewCounterVec(
31 | prometheus.CounterOpts{
32 | Name: cacheMissesName,
33 | Help: cacheMissesHelp,
34 | },
35 | []string{"cache_name"},
36 | )
37 |
38 | // CacheEvictions tracks the number of cache evictions
39 | CacheEvictions = prometheus.NewCounterVec(
40 | prometheus.CounterOpts{
41 | Name: cacheEvictionsName,
42 | Help: cacheEvictionsHelp,
43 | },
44 | []string{"cache_name"},
45 | )
46 |
47 | // CacheItems tracks the current number of items in the cache
48 | CacheItems = prometheus.NewGaugeVec(
49 | prometheus.GaugeOpts{
50 | Name: cacheItemsName,
51 | Help: cacheItemsHelp,
52 | },
53 | []string{"cache_name"},
54 | )
55 | )
56 |
57 | // RegisterMetrics registers all cache metrics with the provided registerer
58 | func RegisterMetrics(reg prometheus.Registerer) {
59 | reg.MustRegister(CacheHits)
60 | reg.MustRegister(CacheMisses)
61 | reg.MustRegister(CacheEvictions)
62 | reg.MustRegister(CacheItems)
63 | }
64 |
--------------------------------------------------------------------------------
/cmd/admin/templates/components/page-header.html:
--------------------------------------------------------------------------------
1 | {{ define "page-header" }}
2 |
3 | {{ with .Metadata }}
4 |
5 |
43 |
44 | {{ end }}
45 |
46 | {{ end }}
47 |
--------------------------------------------------------------------------------
/pkg/logging/none.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/settings"
5 | "github.com/jmpsec/osctrl/pkg/types"
6 | "github.com/rs/zerolog/log"
7 | )
8 |
9 | // LoggerNone will be used to not log any data
10 | type LoggerNone struct {
11 | Enabled bool
12 | }
13 |
14 | // CreateLoggerNone to initialize the logger
15 | func CreateLoggerNone() (*LoggerNone, error) {
16 | return &LoggerNone{Enabled: true}, nil
17 | }
18 |
19 | // Settings - Function to prepare settings for the logger
20 | func (logNone *LoggerNone) Settings(mgr *settings.Settings) {
21 | log.Info().Msg("No none logging settings")
22 | }
23 |
24 | // Log - Function that sends JSON result/status/query logs to stdout
25 | func (logNone *LoggerNone) Log(logType string, data []byte, environment, uuid string, debug bool) {
26 | if debug {
27 | log.Debug().Msgf("Sending %d bytes to none for %s - %s", len(data), environment, uuid)
28 | }
29 | switch logType {
30 | case types.StatusLog:
31 | logNone.Status(data, environment, uuid, debug)
32 | case types.ResultLog:
33 | logNone.Result(data, environment, uuid, debug)
34 | }
35 | }
36 |
37 | // Status - Function that sends JSON status logs to stdout
38 | func (logNone *LoggerNone) Status(data []byte, environment, uuid string, debug bool) {
39 | log.Info().Msgf("Skipping to log %d bytes of status from %s/%s", len(data), environment, uuid)
40 | }
41 |
42 | // Result - Function that sends JSON result logs to stdout
43 | func (logNone *LoggerNone) Result(data []byte, environment, uuid string, debug bool) {
44 | log.Info().Msgf("Skipping to log %d bytes of result from %s/%s", len(data), environment, uuid)
45 | }
46 |
47 | // Query - Function that sends JSON query logs to stdout
48 | func (logNone *LoggerNone) Query(data []byte, environment, uuid, name string, status int, debug bool) {
49 | log.Info().Msgf("Skipping to log %d bytes of query from %s/%s for query %s and status %d", len(data), environment, uuid, name, status)
50 | }
51 |
--------------------------------------------------------------------------------
/deploy/docker/conf/cli/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ENV_NAME="${ENV_NAME:=dev}"
4 | CERT_FILE="${CERT_FILE:=/opt/osctrl/config/osctrl.crt}"
5 | HOST="${HOST:=nginx}"
6 | OSCTRL_USER="${OSCTRL_USER:=admin}"
7 | OSCTRL_PASS="${OSCTRL_PASS:=admin}"
8 | WAIT="${WAIT:=5}"
9 |
10 | ######################################### OSCTRL_PASS #########################################
11 | if [[ -n "$OSCTRL_PASS_FILE" ]]; then
12 | OSCTRL_PASS=$(cat ${OSCTRL_PASS_FILE})
13 | fi
14 |
15 | ######################################### Wait until DB is up #########################################
16 | until /opt/osctrl/bin/osctrl-cli check-db
17 | do
18 | echo "DB is not ready"
19 | sleep $WAIT
20 | done
21 |
22 | ######################################### Create environment #########################################
23 | /opt/osctrl/bin/osctrl-cli --db env add \
24 | --name "${ENV_NAME}" \
25 | --hostname "${HOST}" \
26 | --certificate "${CERT_FILE}"
27 | if [ $? -eq 0 ]; then
28 | echo "Created environment ${ENV_NAME}"
29 | else
30 | echo "Environment ${ENV_NAME} exists"
31 | fi
32 |
33 | ######################################### Create admin user #########################################
34 | /opt/osctrl/bin/osctrl-cli --db user add \
35 | --admin \
36 | --username "${OSCTRL_USER}" \
37 | --password "${OSCTRL_PASS}" \
38 | --environment "${ENV_NAME}" \
39 | --fullname "${OSCTRL_USER}"
40 |
41 | if [ $? -eq 0 ]; then
42 | echo "Created ${OSCTRL_USER} user"
43 | else
44 | echo "The user ${OSCTRL_USER} exists"
45 | fi
46 |
47 | echo "The environment ${ENV_NAME} is ready"
48 |
49 | echo "
50 | ##############################################################################
51 | # Successfully created an osctrl user and env
52 | #
53 | # osctrl admin user: ${OSCTRL_USER}
54 | # osctrl env name: ${ENV_NAME}
55 | ##############################################################################
56 | "
57 |
58 | # Start a shell to avoid re-running this script
59 | /bin/bash
60 |
--------------------------------------------------------------------------------
/cmd/admin/templates/components/page-head-offline.html:
--------------------------------------------------------------------------------
1 | {{ define "page-head" }}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ .Title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{ end }}
53 |
--------------------------------------------------------------------------------
/deploy/docker/dockerfiles/Dockerfile-dev-cli:
--------------------------------------------------------------------------------
1 | #################################################### osctrl-cli-dev ####################################################
2 | ARG GOLANG_VERSION=${GOLANG_VERSION:-1.25.4}
3 | FROM golang:${GOLANG_VERSION} AS osctrl-cli-dev
4 |
5 | WORKDIR /usr/src/app
6 |
7 | ENV GO111MODULE="on"
8 | ENV GOOS="linux"
9 | ENV CGO_ENABLED=0
10 |
11 | # Hot reloading mod
12 | RUN go install github.com/cosmtrek/air@v1.41.0
13 | RUN go install github.com/go-delve/delve/cmd/dlv@v1.20.1
14 |
15 | # Copy code
16 | COPY . /usr/src/app
17 |
18 | # Download deps
19 | RUN go mod download
20 | RUN go mod verify
21 |
22 | ### Copy osctrl-api bin and configs ###
23 | RUN mkdir -p /opt/osctrl/bin
24 | RUN mkdir -p /opt/osctrl/config
25 | RUN go build -ldflags "-s -w -X main.buildCommit=$(git rev-parse HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o /opt/osctrl/bin/osctrl-cli cmd/cli/*.go
26 |
27 | #### User and env init script ####
28 | COPY deploy/docker/conf/cli/entrypoint.sh /opt/osctrl/script/init.sh
29 | RUN chmod +x /opt/osctrl/script/init.sh
30 |
31 | ENTRYPOINT ["air", "-c", "deploy/docker/conf/dev/air/.air-osctrl-cli.toml"]
32 | CMD ["/opt/osctrl/script/init.sh"]
33 |
34 | #################################################### osctrl-ubuntu-osquery ####################################################
35 | FROM osctrl-cli-dev AS osctrl-ubuntu-osquery
36 |
37 | ARG OSQUERY_VERSION
38 |
39 | USER root
40 |
41 | # Install Osquery
42 | RUN apt-get update -y -qq && apt-get install -y curl host
43 | RUN ubuntuArch="$(dpkg --print-architecture)"; \
44 | curl -L https://pkg.osquery.io/deb/osquery_${OSQUERY_VERSION}-1.linux_${ubuntuArch}.deb \
45 | --output /tmp/osquery_${OSQUERY_VERSION}-1.linux_${ubuntuArch}.deb
46 | RUN ubuntuArch="$(dpkg --print-architecture)"; \
47 | dpkg -i /tmp/osquery_${OSQUERY_VERSION}-1.linux_${ubuntuArch}.deb
48 |
49 | # Entrypoint
50 | COPY deploy/docker/conf/osquery/entrypoint.sh /entrypoint.sh
51 | RUN chmod 755 /entrypoint.sh
52 | ENTRYPOINT [ "/entrypoint.sh" ]
53 |
--------------------------------------------------------------------------------
/cmd/admin/templates/components/page-js-offline.html:
--------------------------------------------------------------------------------
1 | {{ define "page-js" }}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ end }}
50 |
--------------------------------------------------------------------------------
/pkg/nodes/models.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // OsqueryNode as abstraction of a node
10 | type OsqueryNode struct {
11 | gorm.Model
12 | NodeKey string `gorm:"index"`
13 | UUID string `gorm:"index"`
14 | Platform string
15 | PlatformVersion string
16 | OsqueryVersion string
17 | Hostname string
18 | Localname string
19 | IPAddress string
20 | Username string
21 | OsqueryUser string
22 | Environment string
23 | CPU string
24 | Memory string
25 | HardwareSerial string
26 | DaemonHash string
27 | ConfigHash string
28 | BytesReceived int
29 | RawEnrollment string
30 | LastSeen time.Time
31 | UserID uint
32 | EnvironmentID uint
33 | ExtraData string
34 | }
35 |
36 | // ArchiveOsqueryNode as abstraction of an archived node
37 | type ArchiveOsqueryNode struct {
38 | gorm.Model
39 | NodeKey string `gorm:"index"`
40 | UUID string `gorm:"index"`
41 | Trigger string
42 | Platform string
43 | PlatformVersion string
44 | OsqueryVersion string
45 | Hostname string
46 | Localname string
47 | IPAddress string
48 | Username string
49 | OsqueryUser string
50 | Environment string
51 | CPU string
52 | Memory string
53 | HardwareSerial string
54 | ConfigHash string
55 | DaemonHash string
56 | BytesReceived int
57 | RawEnrollment string
58 | LastSeen time.Time
59 | UserID uint
60 | EnvironmentID uint
61 | ExtraData string
62 | }
63 |
64 | // NodeMetadata to hold metadata for a node
65 | type NodeMetadata struct {
66 | IPAddress string
67 | Username string
68 | OsqueryUser string
69 | Hostname string
70 | Localname string
71 | ConfigHash string
72 | DaemonHash string
73 | OsqueryVersion string
74 | Platform string
75 | PlatformVersion string
76 | BytesReceived int
77 | }
78 |
--------------------------------------------------------------------------------
/cmd/api/auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/jmpsec/osctrl/cmd/api/handlers"
9 | "github.com/jmpsec/osctrl/pkg/config"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | const (
15 | // Key to identify request context
16 | contextAPI string = "osctrl-api-context"
17 | )
18 |
19 | // Helper to extract token from header
20 | func extractHeaderToken(r *http.Request) string {
21 | reqToken := r.Header.Get("Authorization")
22 | splitToken := strings.Split(reqToken, "Bearer")
23 | if len(splitToken) != 2 {
24 | return ""
25 | }
26 | return strings.TrimSpace(splitToken[1])
27 | }
28 |
29 | // Handler to check access to a resource based on the authentication enabled
30 | func handlerAuthCheck(h http.Handler, auth, jwtSecret string) http.Handler {
31 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32 | switch auth {
33 | case config.AuthNone:
34 | // Set middleware values
35 | s := make(handlers.ContextValue)
36 | s["user"] = "admin"
37 | ctx := context.WithValue(r.Context(), handlers.ContextKey(contextAPI), s)
38 | // Access granted
39 | h.ServeHTTP(w, r.WithContext(ctx))
40 | case config.AuthJWT:
41 | // Set middleware values
42 | token := extractHeaderToken(r)
43 | if token == "" {
44 | http.Redirect(w, r, forbiddenPath, http.StatusForbidden)
45 | return
46 | }
47 | claims, valid := apiUsers.CheckToken(jwtSecret, token)
48 | if !valid {
49 | http.Redirect(w, r, forbiddenPath, http.StatusForbidden)
50 | return
51 | }
52 | // Update metadata for the user
53 | if err := apiUsers.UpdateTokenIPAddress(utils.GetIP(r), claims.Username); err != nil {
54 | log.Err(err).Msgf("error updating token for user %s", claims.Username)
55 | }
56 | // Set middleware values
57 | s := make(handlers.ContextValue)
58 | s["user"] = claims.Username
59 | ctx := context.WithValue(r.Context(), handlers.ContextKey(contextAPI), s)
60 | // Access granted
61 | h.ServeHTTP(w, r.WithContext(ctx))
62 | }
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/functions.js:
--------------------------------------------------------------------------------
1 | function sendGetRequest(req_url, _modal, _callback) {
2 | $.ajax({
3 | url: req_url,
4 | dataType: 'json',
5 | type: 'GET',
6 | contentType: 'application/json',
7 | success: function (data, textStatus, jQxhr) {
8 | console.log('OK');
9 | console.log(data);
10 | if (_modal) {
11 | $("#successModalMessage").text(data.message);
12 | $("#successModal").modal();
13 | }
14 | if (_callback) {
15 | _callback(data);
16 | }
17 | },
18 | error: function (jqXhr, textStatus, errorThrown) {
19 | var _clientmsg = "Client: " + errorThrown;
20 | var _serverJSON = $.parseJSON(jqXhr.responseText);
21 | var _servermsg = 'Server: ' + _serverJSON.message;
22 | $("#errorModalMessageClient").text(_clientmsg);
23 | console.log(_clientmsg);
24 | $("#errorModalMessageServer").text(_servermsg);
25 | $("#errorModal").modal();
26 | }
27 | });
28 | }
29 |
30 | function sendPostRequest(req_data, req_url, _redir, _modal, _callback) {
31 | $.ajax({
32 | url: req_url,
33 | dataType: "json",
34 | type: "POST",
35 | contentType: "application/json",
36 | data: JSON.stringify(req_data),
37 | processData: false,
38 | success: function (data, textStatus, jQxhr) {
39 | console.log("OK");
40 | console.log(data);
41 | if (_modal) {
42 | $("#successModalMessage").text(data.message);
43 | $("#successModal").modal();
44 | }
45 | if (_redir !== "") {
46 | window.location.replace(_redir);
47 | }
48 | if (_callback) {
49 | _callback(data);
50 | }
51 | },
52 | error: function (jqXhr, textStatus, errorThrown) {
53 | var _clientmsg = "Client: " + errorThrown;
54 | var _serverJSON = $.parseJSON(jqXhr.responseText);
55 | var _servermsg = "Server: " + _serverJSON.message;
56 | $("#errorModalMessageClient").text(_clientmsg);
57 | console.log(_clientmsg);
58 | $("#errorModalMessageServer").text(_servermsg);
59 | $("#errorModal").modal();
60 | }
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/admin/handlers/json-stats.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/jmpsec/osctrl/cmd/admin/sessions"
7 | "github.com/jmpsec/osctrl/pkg/nodes"
8 | "github.com/jmpsec/osctrl/pkg/settings"
9 | "github.com/jmpsec/osctrl/pkg/users"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | // Define targets to be used
15 | var (
16 | StatsTargets = map[string]bool{
17 | "environment": true,
18 | "platform": true,
19 | }
20 | )
21 |
22 | // JSONStatsHandler for platform/environment stats in JSON
23 | func (h *HandlersAdmin) JSONStatsHandler(w http.ResponseWriter, r *http.Request) {
24 | if h.DebugHTTPConfig.EnableHTTP {
25 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
26 | }
27 | // Get context data
28 | ctx := r.Context().Value(sessions.ContextKey(sessions.CtxSession)).(sessions.ContextValue)
29 | // Extract stats target
30 | target := r.PathValue("target")
31 | if target == "" {
32 | log.Info().Msg("error getting target")
33 | return
34 | }
35 | // Verify target
36 | if !StatsTargets[target] {
37 | log.Info().Msgf("invalid target %s", target)
38 | return
39 | }
40 | // Extract identifier
41 | identifier := r.PathValue("identifier")
42 | if identifier == "" {
43 | log.Info().Msg("error getting target identifier")
44 | return
45 | }
46 | // Get stats
47 | var stats nodes.StatsData
48 | if target == "environment" {
49 | // Verify identifier
50 | env, err := h.Envs.Get(identifier)
51 | if err != nil {
52 | log.Err(err).Msgf("error getting environment %s", identifier)
53 | return
54 | }
55 | // Check permissions
56 | if !h.Users.CheckPermissions(ctx[sessions.CtxUser], users.UserLevel, env.UUID) {
57 | log.Info().Msgf("%s has insufficient permissions", ctx[sessions.CtxUser])
58 | return
59 | }
60 | stats, err = h.Nodes.GetStatsByEnv(env.Name, h.Settings.InactiveHours(settings.NoEnvironmentID))
61 | if err != nil {
62 | log.Err(err).Msg("error getting stats")
63 | return
64 | }
65 | }
66 | // Serve JSON
67 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, stats)
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/environments/env-cache.go:
--------------------------------------------------------------------------------
1 | package environments
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/jmpsec/osctrl/pkg/cache"
8 | )
9 |
10 | const (
11 | cacheName = "environments"
12 | )
13 |
14 | // EnvCache provides cached access to TLS environments
15 | type EnvCache struct {
16 | // The cache itself, storing Environment objects
17 | cache *cache.MemoryCache[TLSEnvironment]
18 |
19 | // Reference to the environment manager for cache misses
20 | envs EnvManager
21 | }
22 |
23 | // NewEnvCache creates a new environment cache
24 | func NewEnvCache(envs EnvManager) *EnvCache {
25 | // Create a new cache with a 10-minute cleanup interval
26 | envCache := cache.NewMemoryCache(
27 | cache.WithCleanupInterval[TLSEnvironment](2*time.Hour),
28 | cache.WithName[TLSEnvironment](cacheName),
29 | )
30 |
31 | return &EnvCache{
32 | cache: envCache,
33 | envs: envs,
34 | }
35 | }
36 |
37 | // GetByUUID retrieves an environment by UUID, using cache when available
38 | func (ec *EnvCache) GetByUUID(ctx context.Context, uuid string) (TLSEnvironment, error) {
39 | // Try to get from cache first
40 | if env, found := ec.cache.Get(ctx, uuid); found {
41 | return env, nil
42 | }
43 |
44 | // Not in cache, fetch from database
45 | env, err := ec.envs.GetByUUID(uuid)
46 | if err != nil {
47 | return TLSEnvironment{}, err
48 | }
49 |
50 | ec.cache.Set(ctx, uuid, env, 2*time.Hour)
51 |
52 | return env, nil
53 | }
54 |
55 | // InvalidateEnv removes a specific environment from the cache
56 | func (ec *EnvCache) InvalidateEnv(ctx context.Context, uuid string) {
57 | ec.cache.Delete(ctx, uuid)
58 | }
59 |
60 | // InvalidateAll clears the entire cache
61 | func (ec *EnvCache) InvalidateAll(ctx context.Context) {
62 | ec.cache.Clear(ctx)
63 | }
64 |
65 | // UpdateEnvInCache updates an environment in the cache
66 | func (ec *EnvCache) UpdateEnvInCache(ctx context.Context, env TLSEnvironment) {
67 | ec.cache.Set(ctx, env.UUID, env, 2*time.Hour)
68 | }
69 |
70 | // Close stops the cleanup goroutine and releases resources
71 | func (ec *EnvCache) Close() {
72 | if ec.cache != nil {
73 | ec.cache.Stop()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tools/fake_logging.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Script to simulate HTTP logging services (Graylog, Splunk...) for osctrl
5 | #
6 | # Usage: python fake_logging.py port
7 | #
8 |
9 | import http.server
10 | import socketserver
11 | import sys
12 | import time
13 | import json
14 |
15 | _NAME = "FakeServerLogging"
16 | _BIND = "0.0.0.0"
17 | _PARAMS = 2
18 |
19 | _UTF = "utf-8"
20 |
21 |
22 | class FakeServer(http.server.SimpleHTTPRequestHandler):
23 | def _set_headers(self):
24 | self.send_response(200)
25 | self.send_header("Content-type", "application/json")
26 | self.end_headers()
27 |
28 | def do_GET(self):
29 | self._set_headers()
30 | self.wfile.write(bytes("{'text':'Success','code':0}", _UTF))
31 |
32 | def do_POST(self):
33 | content_length = int(self.headers["Content-Length"])
34 | post_data = self.rfile.read(content_length)
35 | self._set_headers()
36 | self.wfile.write(bytes("{'text':'Success','code':0}", _UTF))
37 | print(
38 | "-----------------------------------Headers-----------------------------------------"
39 | )
40 | print(str(self.headers))
41 | print(
42 | "------------------------------------Body-------------------------------------------"
43 | )
44 | print(json.dumps(json.loads(post_data.decode(_UTF)), indent=4))
45 | print(
46 | "-----------------------------------------------------------------------------------"
47 | )
48 |
49 |
50 | if __name__ == "__main__":
51 | if len(sys.argv) < _PARAMS:
52 | print()
53 | print("Usage: " + sys.argv[0] + " port")
54 | exit(1) # pylint: disable=consider-using-sys-exit
55 |
56 | _port = int(sys.argv[1])
57 |
58 | httpd = socketserver.TCPServer((_BIND, _port), FakeServer)
59 | print(f"{time.asctime()},{_NAME} UP - {_BIND}:{_port}")
60 |
61 | try:
62 | httpd.serve_forever()
63 | except KeyboardInterrupt:
64 | pass
65 | httpd.server_close()
66 | print(f"{time.asctime()},{ _NAME} DOWN - {_BIND}:{_port}")
67 |
--------------------------------------------------------------------------------
/cmd/tls/handlers/metrics.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import "github.com/prometheus/client_golang/prometheus"
4 |
5 | const (
6 | RequestPath = "path"
7 | RequestMethod = "method"
8 | StatusCode = "status_code"
9 | Environment = "osctrl_env"
10 | RequestType = "type"
11 | LogType = "log_type"
12 | )
13 |
14 | var (
15 | requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
16 | Name: "osctrl_tls_request_duration_seconds",
17 | Help: "The duration of requests",
18 | Buckets: []float64{0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1, 5},
19 | }, []string{RequestMethod, RequestPath, StatusCode})
20 | requestSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
21 | Name: "osctrl_tls_request_size_bytes",
22 | Help: "The size of requests",
23 | Buckets: []float64{100, 1000, 10000, 100000, 1000000},
24 | }, []string{Environment, RequestType})
25 | logProcessDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
26 | Name: "osctrl_tls_log_process_duration_seconds",
27 | Help: "The duration of log/scheduled query processing",
28 | Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10},
29 | }, []string{Environment, LogType})
30 | distributedQueryProcessingDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
31 | Name: "osctrl_tls_distributed_query_process_duration_seconds",
32 | Help: "The duration of distributed query result processing",
33 | Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10},
34 | }, []string{Environment})
35 | batchFlushDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
36 | Name: "osctrl_tls_batch_flush_duration_seconds",
37 | Help: "The duration of batch data flushing to backend",
38 | Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5},
39 | }, []string{"operation"})
40 | )
41 |
42 | func RegisterMetrics(reg prometheus.Registerer) {
43 | reg.MustRegister(requestDuration)
44 | reg.MustRegister(requestSize)
45 | reg.MustRegister(logProcessDuration)
46 | reg.MustRegister(distributedQueryProcessingDuration)
47 | reg.MustRegister(batchFlushDuration)
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/environments/flags_test.go:
--------------------------------------------------------------------------------
1 | package environments
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestGenServerCertsFlag(t *testing.T) {
8 | t.Run("empty", func(t *testing.T) {
9 | flag := GenServerCertsFlag("")
10 | if flag != "" {
11 | t.Errorf("Expected empty flag, got %s", flag)
12 | }
13 | })
14 | t.Run("not empty", func(t *testing.T) {
15 | flag := GenServerCertsFlag("certificate")
16 | if flag != "--tls_server_certs=certificate" {
17 | t.Errorf("Expected flag --tls_server_certs=certificate, got %s", flag)
18 | }
19 | })
20 | }
21 |
22 | func TestGenCarveBlockSizeFlag(t *testing.T) {
23 | t.Run("empty", func(t *testing.T) {
24 | flag := GenCarveBlockSizeFlag("")
25 | if flag != "" {
26 | t.Errorf("Expected empty flag, got %s", flag)
27 | }
28 | })
29 | t.Run("not empty", func(t *testing.T) {
30 | flag := GenCarveBlockSizeFlag("blockSize")
31 | if flag != "--carver_block_size=blockSize" {
32 | t.Errorf("Expected flag --carver_block_size=blockSize, got %s", flag)
33 | }
34 | })
35 | }
36 |
37 | func TestGenSingleFlag(t *testing.T) {
38 | t.Run("empty", func(t *testing.T) {
39 | flag := GenSingleFlag("tmplName", "flagName", "")
40 | if flag != "--flagName=" {
41 | t.Errorf("Expected --flagName=, got %s", flag)
42 | }
43 | })
44 | t.Run("not empty", func(t *testing.T) {
45 | flag := GenSingleFlag("tmplName", "flagName", "flagValue")
46 | if flag != "--flagName=flagValue" {
47 | t.Errorf("Expected flag --flagName=flagValue, got %s", flag)
48 | }
49 | })
50 | }
51 |
52 | func TestParseFlagTemplate(t *testing.T) {
53 | t.Run("empty data", func(t *testing.T) {
54 | flag := ParseFlagTemplate("tmplName", "flagTemplate", nil)
55 | if flag != "flagTemplate" {
56 | t.Errorf("Expected empty flag, got %s", flag)
57 | }
58 | })
59 | t.Run("not empty data", func(t *testing.T) {
60 | flag := ParseFlagTemplate("tmplName", "--{{ .Name }}={{ .Value }}", struct {
61 | Name string
62 | Value string
63 | }{
64 | Name: "flagName",
65 | Value: "flagValue",
66 | })
67 | if flag != "--flagName=flagValue" {
68 | t.Errorf("Expected flag --flagName=flagValue, got %s", flag)
69 | }
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/deploy/osquery/packs/osctrl-windows-application-security.conf:
--------------------------------------------------------------------------------
1 | {
2 | "platform": "windows",
3 | "queries": {
4 | "bitlocker_autoencrypt_settings_registry": {
5 | "query": "SELECT * FROM registry WHERE key LIKE 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Bitlocker\\%%';",
6 | "interval": 3600,
7 | "description": "Controls Bitlocker full-disk encryption settings."
8 | },
9 | "bitlocker_fde_settings_registry": {
10 | "query": "SELECT * FROM registry WHERE key LIKE 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\FVE\\%%';",
11 | "interval": 3600,
12 | "description": "Controls Bitlocker full-disk encryption settings."
13 | },
14 | "chrome_extension_force_list_registry": {
15 | "query": "SELECT * FROM registry WHERE key='HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome\\ExtensionInstallForcelist';",
16 | "interval": 3600,
17 | "description": "Controls Google Chrome plugins that are forcibly installed."
18 | },
19 | "emet_settings_registry": {
20 | "query": "SELECT * FROM registry WHERE key LIKE 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\EMET\\%%';",
21 | "interval": 3600,
22 | "description": "Controls EMET-protected applications and system settings."
23 | },
24 | "microsoft_laps_settings_registry": {
25 | "query": "SELECT * FROM registry WHERE key='HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft Services\\AdmPwd';",
26 | "interval": 3600,
27 | "description": "Controls Local Administrative Password Solution (LAPS) settings."
28 | },
29 | "passport_for_work_settings_registry": {
30 | "query": "SELECT * FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\PassportForWork\\%%';",
31 | "interval": 3600,
32 | "description": "Controls Windows Passport for Work (Hello) settings."
33 | },
34 | "uac_settings_registry": {
35 | "query": "SELECT * FROM registry WHERE path='HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA';",
36 | "interval": 3600,
37 | "description": "Controls UAC. A setting of 0 indicates that UAC is disabled."
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/admin/settings.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jmpsec/osctrl/pkg/config"
7 | "github.com/jmpsec/osctrl/pkg/settings"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | // Function to load all settings for the service
12 | func loadingSettings(mgr *settings.Settings, cfg *config.ServiceParameters) error {
13 | // Check if service settings for debug service is ready
14 | log.Debug().Msg("Initializing settings")
15 | // Check if service settings for sessions cleanup is ready
16 | if !mgr.IsValue(config.ServiceAdmin, settings.CleanupSessions, settings.NoEnvironmentID) {
17 | if err := mgr.NewIntegerValue(config.ServiceAdmin, settings.CleanupSessions, int64(defaultRefresh), settings.NoEnvironmentID); err != nil {
18 | return fmt.Errorf("failed to add %s to configuration: %w", settings.CleanupSessions, err)
19 | }
20 | }
21 | // Check if service settings for queries/carves cleanup is ready
22 | if !mgr.IsValue(config.ServiceAdmin, settings.CleanupExpired, settings.NoEnvironmentID) {
23 | if err := mgr.NewIntegerValue(config.ServiceAdmin, settings.CleanupExpired, int64(defaultExpiration), settings.NoEnvironmentID); err != nil {
24 | return fmt.Errorf("failed to add %s to configuration: %w", settings.CleanupExpired, err)
25 | }
26 | }
27 | // Check if service settings for node inactive hours is ready
28 | if !mgr.IsValue(config.ServiceAdmin, settings.InactiveHours, settings.NoEnvironmentID) {
29 | if err := mgr.NewIntegerValue(config.ServiceAdmin, settings.InactiveHours, int64(defaultInactive), settings.NoEnvironmentID); err != nil {
30 | return fmt.Errorf("failed to add %s to configuration: %w", settings.InactiveHours, err)
31 | }
32 | }
33 | // Check if service settings for display dashboard is ready
34 | if !mgr.IsValue(config.ServiceAdmin, settings.NodeDashboard, settings.NoEnvironmentID) {
35 | if err := mgr.NewBooleanValue(config.ServiceAdmin, settings.NodeDashboard, false, settings.NoEnvironmentID); err != nil {
36 | return fmt.Errorf("failed to add %s to settings: %w", settings.NodeDashboard, err)
37 | }
38 | }
39 | // Write JSON config to settings
40 | if err := mgr.SetAdminJSON(cfg, settings.NoEnvironmentID); err != nil {
41 | return fmt.Errorf("failed to add JSON values to configuration: %w", err)
42 | }
43 | return nil
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/nodes/node-cache.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/jmpsec/osctrl/pkg/cache"
8 | )
9 |
10 | const (
11 | cacheName = "nodes"
12 | // Default time-to-live for cached nodes
13 | defaultTTL = 60 * time.Minute
14 | // Default cleanup interval for the cache
15 | defaultCleanupInterval = 30 * time.Minute
16 | )
17 |
18 | // NodeCache provides cached access to OsqueryNode objects
19 | type NodeCache struct {
20 | // The cache itself, storing OsqueryNode objects
21 | cache *cache.MemoryCache[OsqueryNode]
22 |
23 | // Reference to the node manager for cache misses
24 | nodes *NodeManager
25 | }
26 |
27 | // NewNodeCache creates a new node cache
28 | func NewNodeCache(nodes *NodeManager) *NodeCache {
29 | // Create a new cache with appropriate cleanup interval
30 | nodeCache := cache.NewMemoryCache(
31 | cache.WithCleanupInterval[OsqueryNode](defaultCleanupInterval),
32 | cache.WithName[OsqueryNode](cacheName),
33 | )
34 |
35 | return &NodeCache{
36 | cache: nodeCache,
37 | nodes: nodes,
38 | }
39 | }
40 |
41 | // GetByKey retrieves a node by node_key, using cache when available
42 | func (nc *NodeCache) GetByKey(ctx context.Context, nodeKey string) (OsqueryNode, error) {
43 | // Try to get from cache first
44 | if node, found := nc.cache.Get(ctx, nodeKey); found {
45 | return node, nil
46 | }
47 |
48 | // Not in cache, fetch from database
49 | node, err := nc.nodes.getByKeyFromDB(nodeKey)
50 | if err != nil {
51 | return OsqueryNode{}, err
52 | }
53 |
54 | // Store in cache for future requests
55 | nc.cache.Set(ctx, nodeKey, node, defaultTTL)
56 |
57 | return node, nil
58 | }
59 |
60 | // InvalidateNode removes a specific node from the cache
61 | func (nc *NodeCache) InvalidateNode(ctx context.Context, nodeKey string) {
62 | nc.cache.Delete(ctx, nodeKey)
63 | }
64 |
65 | // InvalidateAll clears the entire cache
66 | func (nc *NodeCache) InvalidateAll(ctx context.Context) {
67 | nc.cache.Clear(ctx)
68 | }
69 |
70 | // UpdateNodeInCache updates a node in the cache
71 | func (nc *NodeCache) UpdateNodeInCache(ctx context.Context, node OsqueryNode) {
72 | nc.cache.Set(ctx, node.NodeKey, node, defaultTTL)
73 | }
74 |
75 | // Close stops the cleanup goroutine and releases resources
76 | func (nc *NodeCache) Close() {
77 | if nc.cache != nil {
78 | nc.cache.Stop()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/api/handlers/login.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/jmpsec/osctrl/pkg/types"
9 | "github.com/jmpsec/osctrl/pkg/users"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | )
12 |
13 | // LoginHandler - POST Handler for API login request
14 | func (h *HandlersApi) LoginHandler(w http.ResponseWriter, r *http.Request) {
15 | // Debug HTTP if enabled, never log the body for login
16 | if h.DebugHTTPConfig.EnableHTTP {
17 | utils.DebugHTTPDump(h.DebugHTTP, r, false)
18 | }
19 | // Extract environment
20 | envVar := r.PathValue("env")
21 | if envVar == "" {
22 | apiErrorResponse(w, "error with environment", http.StatusBadRequest, nil)
23 | return
24 | }
25 | // Get environment by UUID
26 | env, err := h.Envs.GetByUUID(envVar)
27 | if err != nil {
28 | apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil)
29 | return
30 | }
31 | var l types.ApiLoginRequest
32 | // Parse request JSON body
33 | if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
34 | apiErrorResponse(w, "error parsing POST body", http.StatusInternalServerError, err)
35 | return
36 | }
37 | // Check credentials
38 | access, user := h.Users.CheckLoginCredentials(l.Username, l.Password)
39 | if !access {
40 | apiErrorResponse(w, "invalid credentials", http.StatusForbidden, err)
41 | return
42 | }
43 | // Check if user has access to this environment
44 | if !h.Users.CheckPermissions(l.Username, users.AdminLevel, env.UUID) {
45 | apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use %s by user %s", h.ServiceName, l.Username))
46 | return
47 | }
48 | // Do we have a token already?
49 | if user.APIToken == "" {
50 | token, exp, err := h.Users.CreateToken(l.Username, h.ServiceName, l.ExpHours)
51 | if err != nil {
52 | apiErrorResponse(w, "error creating token", http.StatusInternalServerError, err)
53 | return
54 | }
55 | if err = h.Users.UpdateToken(l.Username, token, exp); err != nil {
56 | apiErrorResponse(w, "error updating token", http.StatusInternalServerError, err)
57 | return
58 | }
59 | user.APIToken = token
60 | }
61 | h.AuditLog.NewLogin(l.Username, r.RemoteAddr)
62 | // Serialize and serve JSON
63 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiLoginResponse{Token: user.APIToken})
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/queries/saved.go:
--------------------------------------------------------------------------------
1 | package queries
2 |
3 | import (
4 | "fmt"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // SavedQuery as abstraction of a saved query to be used in distributed, schedule or packs
10 | type SavedQuery struct {
11 | gorm.Model
12 | Name string
13 | Creator string
14 | Query string
15 | EnvironmentID uint
16 | ExtraData string
17 | }
18 |
19 | // GetSavedByCreator to get a saved query by creator
20 | func (q *Queries) GetSavedByCreator(creator string, envid uint) ([]SavedQuery, error) {
21 | var saved []SavedQuery
22 | if err := q.DB.Where("creator = ? AND environment_id = ?", creator, envid).Find(&saved).Error; err != nil {
23 | return saved, err
24 | }
25 | return saved, nil
26 | }
27 |
28 | // GetSaved to get a saved query by creator
29 | func (q *Queries) GetSaved(name, creator string, envid uint) (SavedQuery, error) {
30 | var saved SavedQuery
31 | if err := q.DB.Where("creator = ? AND name = ? AND environment_id = ?", creator, name, envid).Find(&saved).Error; err != nil {
32 | return saved, err
33 | }
34 | return saved, nil
35 | }
36 |
37 | // CreateSaved to create new saved query
38 | func (q *Queries) CreateSaved(name, query, creator string, envid uint) error {
39 | saved := SavedQuery{
40 | Name: name,
41 | Query: query,
42 | Creator: creator,
43 | EnvironmentID: envid,
44 | }
45 | if err := q.DB.Create(&saved).Error; err != nil {
46 | return err
47 | }
48 | return nil
49 | }
50 |
51 | // UpdateSaved to update an existing saved query
52 | func (q *Queries) UpdateSaved(name, query, creator string, envid uint) error {
53 | saved, err := q.GetSaved(name, creator, envid)
54 | if err != nil {
55 | return fmt.Errorf("error getting saved query %w", err)
56 | }
57 | data := SavedQuery{
58 | Name: name,
59 | Query: query,
60 | EnvironmentID: envid,
61 | }
62 | if err := q.DB.Model(&saved).Updates(data).Error; err != nil {
63 | return fmt.Errorf("in Updates %w", err)
64 | }
65 | return nil
66 | }
67 |
68 | // DeleteSaved to delete an existing saved query
69 | func (q *Queries) DeleteSaved(name, creator string, envid uint) error {
70 | saved, err := q.GetSaved(name, creator, envid)
71 | if err != nil {
72 | return fmt.Errorf("error getting saved query %w", err)
73 | }
74 | if err := q.DB.Unscoped().Delete(&saved).Error; err != nil {
75 | return fmt.Errorf("in DeleteSaved %w", err)
76 | }
77 | return nil
78 | }
79 |
--------------------------------------------------------------------------------
/deploy/docker/conf/nginx/osctrl.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 443 ssl default deferred;
3 |
4 | ssl_certificate /etc/ssl/certs/osctrl.crt;
5 | ssl_certificate_key /etc/ssl/private/osctrl.key;
6 |
7 | # Improve HTTPS performance with session resumption
8 | ssl_session_cache shared:SSL:10m;
9 | ssl_session_timeout 5m;
10 |
11 | # Enable server-side protection against BEAST attacks
12 | ssl_prefer_server_ciphers on;
13 | ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
14 |
15 | # Disable SSLv3
16 | ssl_protocols TLSv1.2 TLSv1.3;
17 |
18 | # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox)
19 | ssl_stapling on;
20 | ssl_stapling_verify on;
21 | ssl_trusted_certificate /etc/ssl/certs/osctrl.crt;
22 | resolver 1.1.1.1 8.8.8.8 valid=300s;
23 | resolver_timeout 5s;
24 |
25 | # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security)
26 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
27 | add_header X-Frame-Options DENY;
28 |
29 | add_header Cache-Control "no-cache, no-store";
30 | add_header Pragma "no-cache";
31 | expires -1;
32 |
33 | ################################## osctrl-admin webgui ##################################
34 | location / {
35 | proxy_set_header Host $host;
36 | proxy_set_header X-Real-IP $remote_addr;
37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38 | proxy_set_header X-Forwarded-Proto $scheme;
39 |
40 | # Fix the “It appears that your reverse proxy set up is broken" error.
41 | proxy_pass http://osctrl-admin:9001;
42 | proxy_read_timeout 90;
43 | }
44 |
45 | ################################## osctrl-tls osquery server ##################################
46 | location ~* .(enroll|config|log|init|block|read|write|enroll.sh|enroll.ps1|remove.sh|remove.ps1)$ {
47 | proxy_set_header Host $host;
48 | proxy_set_header X-Real-IP $remote_addr;
49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
50 | proxy_set_header X-Forwarded-Proto $scheme;
51 |
52 | # Fix the “It appears that your reverse proxy set up is broken" error.
53 | proxy_pass http://osctrl-tls:9000;
54 | proxy_read_timeout 90;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/deploy/cicd/deb/pre-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | ################### Create osctrl user and group ###################
6 | id -u osctrl &>/dev/null || adduser --system --no-create-home --group osctrl
7 |
8 | ################### Create osctrl directory ###################
9 | if [ ! -d "/opt/osctrl" ]
10 | then
11 | echo "Directory /opt/osctrl DOES NOT exists."
12 | mkdir /opt/osctrl
13 | chown root:root -R /opt/osctrl
14 | fi
15 |
16 | ################### Create osctrl log directory ###################
17 | if [ ! -d "/var/log/osctrl-{{ OSCTRL_COMPONENT }}" ]
18 | then
19 | echo "Directory /var/log/osctrl-{{ OSCTRL_COMPONENT }} DOES NOT exists."
20 | mkdir /var/log/osctrl-{{ OSCTRL_COMPONENT }}
21 | chown osctrl:adm -R /var/log/osctrl-{{ OSCTRL_COMPONENT }}
22 | fi
23 |
24 |
25 | ################### Set perms on config directory ###################
26 | if [ -d "/opt/osctrl/config" ]
27 | then
28 | chown root:osctrl -R /opt/osctrl/config
29 | fi
30 |
31 | ################### Copy needed configs ###################
32 | if [ ! -f /opt/osctrl/config/db.json ]
33 | then
34 | cp /tmp/osctrl-{{ OSCTRL_COMPONENT }}/db.json.example /opt/osctrl/config/db.json
35 | chown root:root /opt/osctrl/config/db.json.example
36 | fi
37 |
38 | if [ ! -f /opt/osctrl/config/redis.json ]
39 | then
40 | cp /tmp/osctrl-{{ OSCTRL_COMPONENT }}/redis.json.example /opt/osctrl/config/redis.json
41 | chown root:root /opt/osctrl/config/redis.json.example
42 | fi
43 | rm -rd /tmp/osctrl-{{ OSCTRL_COMPONENT }}
44 |
45 | ################### osctrl-admin web assets ###################
46 | if [ -d "/opt/osctrl/tmpl_admin" ]
47 | then
48 | # set user as the owner
49 | chown root -R /opt/osctrl/tmpl_admin/
50 |
51 | # set osctrl as the group owner
52 | chgrp -R osctrl /opt/osctrl/tmpl_admin/
53 |
54 | # 750 permissions for everything
55 | chmod -R 750 /opt/osctrl/tmpl_admin/
56 |
57 | # new files and folders inherit group ownership from the parent folder
58 | chmod g+s /opt/osctrl/tmpl_admin/
59 | fi
60 |
61 | if [ -d "/opt/osctrl/static" ]
62 | then
63 | # set user as the owner
64 | chown root -R /opt/osctrl/static/
65 |
66 | # set osctrl as the group owner
67 | chgrp -R osctrl /opt/osctrl/static/
68 |
69 | # 750 permissions for everything
70 | chmod -R 750 /opt/osctrl/static/
71 |
72 | # new files and folders inherit group ownership from the parent folder
73 | chmod g+s /opt/osctrl/static/
74 | fi
75 |
--------------------------------------------------------------------------------
/.github/actions/build/binaries/action.yml:
--------------------------------------------------------------------------------
1 | name: "Build Osctrl binaries"
2 | description: "Build Osctrl components with Golang"
3 | inputs:
4 | go_os:
5 | required: true
6 | description: Define the OS to compile binary for - https://pkg.go.dev/internal/goos
7 | go_arch:
8 | required: true
9 | description: Define the architecture to compile binary for - https://pkg.go.dev/internal/goarch
10 | osctrl_component:
11 | required: true
12 | description: Define the osctrl component to compile
13 | commit_sha:
14 | required: true
15 | description: Define the SHA1 git commit hash
16 | commit_branch:
17 | required: true
18 | description: Define the git branch
19 | golang_version:
20 | required: false
21 | description: Define the version of golang to compile with
22 | default: 1.25.4
23 |
24 | runs:
25 | using: "composite"
26 | steps:
27 | ########################### Checkout code ###########################
28 | - name: Checkout code
29 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
30 | with:
31 | fetch-depth: 2
32 |
33 | ########################### Install go to env ###########################
34 | - name: Set up Go
35 | uses: actions/setup-go@8e57b58e57be52ac95949151e2777ffda8501267 # v5.5.0
36 | with:
37 | go-version: ${{ inputs.golang_version }}
38 | - run: go version
39 | shell: bash
40 |
41 | ########################### Get GO deps #############################
42 | - name: Get GO deps
43 | run: go mod download
44 | shell: bash
45 |
46 | ########################### Build osctrl inputs.osctrl_component ###########################
47 | - name: Build osctrl component
48 | run: |
49 | GOOS=${{ inputs.go_os }} GOARCH=${{ inputs.go_arch }} \
50 | go build -o osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin \
51 | ./cmd/${{ inputs.osctrl_component }}
52 | shell: bash
53 |
54 | ########################### Upload artifacts ###########################
55 | - name: Upload osctrl binaries
56 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
57 | with:
58 | name: osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin
59 | path: osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin
60 | retention-days: 10
61 |
--------------------------------------------------------------------------------
/pkg/environments/oneliners_test.go:
--------------------------------------------------------------------------------
1 | package environments
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestPrepareOneLiner(t *testing.T) {
10 | envTest := TLSEnvironment{
11 | Certificate: "certificate",
12 | Hostname: "hostname",
13 | UUID: "name",
14 | RemoveSecretPath: "rPath",
15 | EnrollSecretPath: "ePath",
16 | }
17 | tmpl := "oneliner {{ .InsecureTLS }} 1 {{ .TLSHost }} 2 {{ .Environment }} 3 {{ .SecretPath }}"
18 | t.Run("empty", func(t *testing.T) {
19 | oneliner, _ := PrepareOneLiner("", true, TLSEnvironment{}, "")
20 | assert.Equal(t, oneliner, "")
21 | })
22 | t.Run("not empty insecure enroll.sh", func(t *testing.T) {
23 | oneliner, _ := PrepareOneLiner(tmpl, true, envTest, "enroll.sh")
24 | assert.Equal(t, oneliner, "oneliner k 1 hostname 2 name 3 ePath")
25 | })
26 | t.Run("not empty insecure remove.sh", func(t *testing.T) {
27 | oneliner, _ := PrepareOneLiner(tmpl, true, envTest, "remove.sh")
28 | assert.Equal(t, oneliner, "oneliner k 1 hostname 2 name 3 rPath")
29 | })
30 | t.Run("not empty insecure enroll.ps1", func(t *testing.T) {
31 | oneliner, _ := PrepareOneLiner(tmpl, true, envTest, "enroll.ps1")
32 | assert.Equal(t, oneliner, "oneliner [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; 1 hostname 2 name 3 ePath")
33 | })
34 | t.Run("not empty insecure remove.ps1", func(t *testing.T) {
35 | oneliner, _ := PrepareOneLiner(tmpl, true, envTest, "remove.ps1")
36 | assert.Equal(t, oneliner, "oneliner [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; 1 hostname 2 name 3 rPath")
37 | })
38 | // Empty certificate means secure TLS
39 | envTest.Certificate = ""
40 | t.Run("not empty secure enroll.sh", func(t *testing.T) {
41 | oneliner, _ := PrepareOneLiner(tmpl, false, envTest, "enroll.sh")
42 | assert.Equal(t, oneliner, "oneliner 1 hostname 2 name 3 ePath")
43 | })
44 | t.Run("not empty secure remove.sh", func(t *testing.T) {
45 | oneliner, _ := PrepareOneLiner(tmpl, false, envTest, "remove.sh")
46 | assert.Equal(t, oneliner, "oneliner 1 hostname 2 name 3 rPath")
47 | })
48 | t.Run("not empty secure enroll.ps1", func(t *testing.T) {
49 | oneliner, _ := PrepareOneLiner(tmpl, false, envTest, "enroll.ps1")
50 | assert.Equal(t, oneliner, "oneliner 1 hostname 2 name 3 ePath")
51 | })
52 | t.Run("not empty secure remove.ps1", func(t *testing.T) {
53 | oneliner, _ := PrepareOneLiner(tmpl, false, envTest, "remove.ps1")
54 | assert.Equal(t, oneliner, "oneliner 1 hostname 2 name 3 rPath")
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/environments/util.go:
--------------------------------------------------------------------------------
1 | package environments
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "time"
8 | )
9 |
10 | // ReadExternalFile to read an external file and return contents
11 | func ReadExternalFile(path string) string {
12 | content, err := os.ReadFile(path)
13 | if err != nil {
14 | return ""
15 | }
16 | return string(content)
17 | }
18 |
19 | // IsItExpired to determine if a time has expired, which makes it in the past
20 | func IsItExpired(t time.Time) bool {
21 | if t.IsZero() {
22 | return false
23 | }
24 | now := time.Now()
25 | return (int(t.Sub(now).Seconds()) <= 0)
26 | }
27 |
28 | // IsPlatformQuery to know if a plaform is going to trigger a query
29 | func IsPlatformQuery(pQuery, pCheck string) bool {
30 | // Empty plaform means all platforms
31 | if pQuery == "" || pQuery == "all" || pQuery == "any" {
32 | return true
33 | }
34 | // Check if platform is posix (darwin, freebsd, linux)
35 | if pQuery == "posix" && (pCheck == "darwin" || pCheck == "freebsd" || pCheck == "linux" || IsPlatformLinux(strings.ToLower(pCheck))) {
36 | return true
37 | }
38 | // Last check is platform itself
39 | return (pQuery == pCheck)
40 | }
41 |
42 | // IsPlatformLinux to know if a linux is going to trigger a query
43 | func IsPlatformLinux(pCheck string) bool {
44 | return (pCheck == "ubuntu" || pCheck == "centos" || pCheck == "rhel" || pCheck == "fedora" || pCheck == "debian" || pCheck == "opensuse" || pCheck == "arch" || pCheck == "amzn")
45 | }
46 |
47 | // PackageDownloadURL to get the download URL for a package
48 | func PackageDownloadURL(env TLSEnvironment, pkg string) string {
49 | if pkg == "" {
50 | return ""
51 | }
52 | if strings.HasPrefix(pkg, "https://") {
53 | return pkg
54 | }
55 | return fmt.Sprintf("https://%s/%s/%s/package/%s", env.Hostname, env.UUID, env.Secret, pkg)
56 | }
57 |
58 | // EnvironmentFinderID to find the environment and return its name based on the environment ID
59 | func EnvironmentFinderID(envID uint, envs []TLSEnvironment, uuid bool) string {
60 | if envID == 0 {
61 | return "None"
62 | }
63 | for _, env := range envs {
64 | if env.ID == envID {
65 | if uuid {
66 | return env.UUID
67 | }
68 | return env.Name
69 | }
70 | }
71 | return "Unknown"
72 | }
73 |
74 | // EnvironmentFinderUUID to find the environment and return its name based on the environment UUID
75 | func EnvironmentFinderUUID(envIdentifier string, envs []TLSEnvironment) string {
76 | for _, env := range envs {
77 | if env.UUID == envIdentifier || env.Name == envIdentifier {
78 | return env.Name
79 | }
80 | }
81 | return "Unknown"
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/logging/s3.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | osctrl_config "github.com/jmpsec/osctrl/pkg/config"
11 | "github.com/jmpsec/osctrl/pkg/settings"
12 | "github.com/rs/zerolog/log"
13 |
14 | "github.com/aws/aws-sdk-go-v2/aws"
15 | "github.com/aws/aws-sdk-go-v2/config"
16 | "github.com/aws/aws-sdk-go-v2/credentials"
17 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
18 | "github.com/aws/aws-sdk-go-v2/service/s3"
19 | )
20 |
21 | // LoggerS3 will be used to log data using S3
22 | type LoggerS3 struct {
23 | S3Config osctrl_config.S3Logger
24 | AWSConfig aws.Config
25 | Client *s3.Client
26 | Uploader *manager.Uploader
27 | Enabled bool
28 | Debug bool
29 | }
30 |
31 | // CreateLoggerS3 to initialize the logger
32 | func CreateLoggerS3(s3Config *osctrl_config.S3Logger) (*LoggerS3, error) {
33 | ctx := context.Background()
34 | creds := credentials.NewStaticCredentialsProvider(s3Config.AccessKey, s3Config.SecretAccessKey, "")
35 | cfg, err := config.LoadDefaultConfig(
36 | ctx,
37 | config.WithCredentialsProvider(creds), config.WithRegion(s3Config.Region),
38 | )
39 | if err != nil {
40 | return nil, err
41 | }
42 | client := s3.NewFromConfig(cfg)
43 | uploader := manager.NewUploader(client)
44 | l := &LoggerS3{
45 | S3Config: *s3Config,
46 | AWSConfig: cfg,
47 | Client: client,
48 | Uploader: uploader,
49 | Enabled: true,
50 | Debug: false,
51 | }
52 | return l, nil
53 | }
54 |
55 | // Settings - Function to prepare settings for the logger
56 | func (logS3 *LoggerS3) Settings(mgr *settings.Settings) {
57 | log.Info().Msg("No s3 logging settings")
58 | }
59 |
60 | // Send - Function that sends JSON logs to S3
61 | func (logS3 *LoggerS3) Send(logType string, data []byte, environment, uuid string, debug bool) {
62 | ctx := context.Background()
63 | if debug {
64 | log.Debug().Msgf("Sending %d bytes to S3 for %s - %s", len(data), environment, uuid)
65 | }
66 | ptrContentLength := int64(len(data))
67 | result, err := logS3.Uploader.Upload(ctx, &s3.PutObjectInput{
68 | Bucket: aws.String(logS3.S3Config.Bucket),
69 | Key: aws.String(environment + "/" + logType + "/" + uuid + ":" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".json"),
70 | Body: bytes.NewBuffer(data),
71 | ContentLength: &ptrContentLength,
72 | ContentType: aws.String(http.DetectContentType(data)),
73 | })
74 | if err != nil {
75 | log.Err(err).Msg("Error sending data to s3")
76 | }
77 | if debug {
78 | log.Debug().Msgf("S3 Upload %+v", result)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # osctrl
2 |
3 |
4 |
5 |
6 | Fast and efficient osquery management.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## What is osctrl?
22 |
23 | **osctrl** is a fast and efficient [osquery](https://osquery.io) management solution, implementing its [remote API](https://osquery.readthedocs.io/en/stable/deployment/remote/) as TLS endpoint.
24 |
25 | With **osctrl** you can monitor all your systems running osquery, distribute its configuration fast, collect all the status and result logs and allow you to run on-demand queries.
26 |
27 | > [!WARNING]
28 | > **osctrl** is a fast evolving project, and while it is already being used in production environments, it is still under active development. Please make sure to read the documentation and understand its current state before deploying it in a critical environment.
29 |
30 | ## Running osctrl with docker for development
31 |
32 | You can use docker to run **osctrl** and all the components are defined in the `docker-compose-dev.yml` that ties all the components together, to serve a functional deployment.
33 |
34 | Ultimately you can just execute `make docker_dev` and it will automagically build and run **osctrl** locally in docker, for development purposes.
35 |
36 | ## Documentation
37 |
38 | You can find the documentation of the project in [https://osctrl.net](https://osctrl.net)
39 |
40 | ## Slack
41 |
42 | Find us in the #osctrl channel in the official osquery Slack community ([Request an auto-invite!](https://join.slack.com/t/osquery/shared_invite/zt-1wipcuc04-DBXmo51zYJKBu3_EP3xZPA))
43 |
44 | ## License
45 |
46 | **osctrl** is licensed under the [MIT License](https://github.com/jmpsec/osctrl/blob/master/LICENSE).
47 |
48 | ## Contributing
49 |
50 | Feel free to fork the repository and submit pull requests. For major changes, please open an issue first to discuss what you would like to change.
51 |
--------------------------------------------------------------------------------
/pkg/nodes/utils.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // For testing - allows us to mock time.Now()
10 | var timeNow = time.Now
11 |
12 | // IsActive determines if a node is active based on when it was last seen.
13 | // The inactive parameter specifies the number of hours a node can be without
14 | // checking in before it's considered inactive. This number is expected positive.
15 | // Returns true if the node has checked in within the specified timeframe.
16 | func IsActive(n OsqueryNode, inactive int64) bool {
17 | // If LastSeen is zero (never seen), node is not active
18 | if n.LastSeen.IsZero() {
19 | return false
20 | }
21 | // A node is active if it was seen more recently than the inactive threshold
22 | cutoffTime := ActiveTimeCutoff(inactive)
23 | return n.LastSeen.After(cutoffTime)
24 | }
25 |
26 | // ActiveTimeCutoff returns the cutoff time for active nodes
27 | // based on the specified number of hours
28 | func ActiveTimeCutoff(hours int64) time.Time {
29 | return timeNow().Add(-time.Duration(hours) * time.Hour)
30 | }
31 |
32 | // ApplyNodeTarget adds the appropriate query constraints for the target node status
33 | // (active, inactive, all) to the provided gorm query. Default is all nodes.
34 | func ApplyNodeTarget(query *gorm.DB, target string, hours int64) *gorm.DB {
35 | switch target {
36 | case AllNodes:
37 | return query
38 | case ActiveNodes:
39 | cutoff := ActiveTimeCutoff(hours)
40 | return query.Where("last_seen > ?", cutoff)
41 | case InactiveNodes:
42 | cutoff := ActiveTimeCutoff(hours)
43 | return query.Where("last_seen <= ?", cutoff)
44 | default:
45 | return query
46 | }
47 | }
48 |
49 | // GetStats retrieves node statistics (total, active, inactive) for the given filter condition
50 | func GetStats(db *gorm.DB, column, value string, hours int64) (StatsData, error) {
51 | var stats StatsData
52 |
53 | // Base query with the filter condition
54 | baseQuery := db.Model(&OsqueryNode{}).Where(column+" = ?", value)
55 |
56 | // Get total count
57 | if err := baseQuery.Count(&stats.Total).Error; err != nil {
58 | return stats, err
59 | }
60 |
61 | // Get active count - nodes seen after the cutoff time
62 | cutoff := ActiveTimeCutoff(hours)
63 | activeQuery := db.Model(&OsqueryNode{}).Where(column+" = ?", value).Where("last_seen > ?", cutoff)
64 | if err := activeQuery.Count(&stats.Active).Error; err != nil {
65 | return stats, err
66 | }
67 |
68 | // Get inactive count
69 | // Calculate inactive count as total - active to be consistent
70 | stats.Inactive = stats.Total - stats.Active
71 |
72 | return stats, nil
73 | }
74 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_test_pr.yml:
--------------------------------------------------------------------------------
1 | name: Build and test PRs pushed to osctrl
2 | permissions:
3 | contents: read
4 |
5 | on: [push, pull_request]
6 |
7 | env:
8 | GOLANG_VERSION: 1.25.4
9 | OSQUERY_VERSION: 5.20.0
10 |
11 | jobs:
12 | build_and_test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | components: ["tls", "admin", "api", "cli"]
17 | goos: ["linux", "darwin", "windows"]
18 | goarch: ["amd64", "arm64"]
19 | steps:
20 | ########################### Checkout code ###########################
21 | - name: Checkout code
22 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
23 | with:
24 | fetch-depth: 2
25 |
26 | ########################### Generate SHA1 commit hash ###########################
27 | # https://newbedev.com/getting-current-branch-and-commit-hash-in-github-action
28 | - name: Declare GIT hash and branch
29 | id: vars
30 | shell: bash
31 | run: |
32 | echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
33 | echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT
34 | echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
35 |
36 | ########################### Build osctrl ###########################
37 | - name: Build osctrl binaries
38 | # Build all osctrl components for linux for all archs
39 | # Build all osctrl components for darwin for all archs
40 | # Build osctrl cli for windows for all archs
41 | if: matrix.goos == 'linux' || matrix.goos == 'darwin' || (matrix.goos == 'windows' && matrix.components == 'cli')
42 | uses: ./.github/actions/build/binaries
43 | with:
44 | go_os: "${{ matrix.goos }}"
45 | go_arch: "${{ matrix.goarch }}"
46 | osctrl_component: "${{ matrix.components }}"
47 | commit_sha: "${{ steps.vars.outputs.sha_short }}"
48 | commit_branch: "${{ steps.vars.outputs.branch }}"
49 | golang_version: "${{ env.GOLANG_VERSION }}"
50 |
51 | ########################### Test binaries ###########################
52 | # - name: Run tests
53 | # id: bin_tests
54 | # uses: ./.github/actions/test/binaries
55 | # with:
56 | # go_os: "${{ matrix.goos }}"
57 | # go_arch: "${{ matrix.goarch }}"
58 | # osctrl_component: "${{ matrix.components }}"
59 | # commit_sha: "${{ steps.vars.outputs.sha_short }}"
60 | # commit_branch: "${{ steps.vars.outputs.branch }}"
61 | # golang_version: "${{ env.GOLANG_VERSION }}"
62 |
--------------------------------------------------------------------------------
/.github/actions/test/binaries/action.yml:
--------------------------------------------------------------------------------
1 | name: "Test Osctrl binaries"
2 | description: "Run Osctrl tests"
3 | inputs:
4 | go_os:
5 | required: true
6 | description: Define the OS to compile binary for - https://pkg.go.dev/internal/goos
7 | go_arch:
8 | required: true
9 | description: Define the architecture to compile binary for - https://pkg.go.dev/internal/goarch
10 | osctrl_component:
11 | required: true
12 | description: Define the osctrl component to compile
13 | commit_sha:
14 | required: true
15 | description: Define the SHA1 git commit hash
16 | commit_branch:
17 | required: true
18 | description: Define the git branch
19 | golang_version:
20 | required: false
21 | description: Define the version of golang to compile with
22 | default: 1.25.4
23 |
24 | runs:
25 | using: "composite"
26 | steps:
27 | ########################### Install go to env ###########################
28 | - name: Set up Go
29 | uses: actions/setup-go@8e57b58e57be52ac95949151e2777ffda8501267 # v5.5.0
30 | with:
31 | go-version: ${{ inputs.golang_version }}
32 | - run: go version
33 | shell: bash
34 |
35 | ########################### Checkout code ###########################
36 | - name: Checkout code
37 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
38 |
39 | ########################### Lint golang code ###########################
40 | - name: golangci-lint
41 | uses: golangci/golangci-lint-action@e60da84bfae8c7920a47be973d75e15710aa8bd7 # v8.0.0
42 | with:
43 | version: v1.29
44 |
45 | ########################### Get GO deps #############################
46 | - name: Get GO deps
47 | run: go mod download
48 | shell: bash
49 |
50 | ########################### Download artifacts ###########################
51 | - name: Download a osctrl binaries
52 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
53 | with:
54 | name: osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_branch }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin
55 |
56 | ########################### Run tests ###########################
57 | - name: Run tests - go clean
58 | run: go clean -testcache ./...
59 | shell: bash
60 |
61 | - name: Run tests - go clean
62 | run: go test ./utils -v
63 | shell: bash
64 |
65 | - name: Run tests - go clean
66 | run: go test ./tls/handlers -v
67 | shell: bash
68 |
69 | - name: Run tests - go clean
70 | run: go clean -testcache ./...
71 | shell: bash
72 |
--------------------------------------------------------------------------------
/cmd/tls/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/config"
5 | "github.com/jmpsec/osctrl/pkg/environments"
6 | "github.com/jmpsec/osctrl/pkg/settings"
7 | "github.com/rs/zerolog/log"
8 | )
9 |
10 | // Helper to determine if an IPv4 is public, based on the following:
11 | // Class Starting IPAddress Ending IPAddress
12 | // A 10.0.0.0 10.255.255.255
13 | // B 172.16.0.0 172.31.255.255
14 | // C 192.168.0.0 192.168.255.255
15 | // Link-local 169.254.0.0 169.254.255.255
16 | // Local 127.0.0.0 127.255.255.255
17 | /*
18 | func isPublicIP(ip net.IP) bool {
19 | // Use native functions
20 | if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
21 | return false
22 | }
23 | // Check each octet
24 | if ip4 := ip.To4(); ip4 != nil {
25 | switch {
26 | case ip4[0] == 10:
27 | return false
28 | case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
29 | return false
30 | case ip4[0] == 192 && ip4[1] == 168:
31 | return false
32 | default:
33 | return true
34 | }
35 | }
36 | return false
37 | }
38 | */
39 |
40 | // Helper to refresh the environments map until cache/Redis support is implemented
41 | func refreshEnvironments() environments.MapEnvironments {
42 | log.Debug().Msg("Refreshing environments...")
43 | _envsmap, err := envs.GetMap()
44 | if err != nil {
45 | log.Err(err).Msg("error refreshing environments")
46 | return environments.MapEnvironments{}
47 | }
48 | return _envsmap
49 | }
50 |
51 | // Helper to refresh the settings until cache/Redis support is implemented
52 | func refreshSettings() settings.MapSettings {
53 | log.Debug().Msg("Refreshing settings...")
54 | _settingsmap, err := settingsmgr.GetMap(config.ServiceTLS, settings.NoEnvironmentID)
55 | if err != nil {
56 | log.Err(err).Msg("error refreshing settings")
57 | return settings.MapSettings{}
58 | }
59 | return _settingsmap
60 | }
61 |
62 | // Helper to convert YAML settings loaded from file to settings
63 | func loadedYAMLToServiceParams(yml config.TLSConfiguration, loadedFile string) *config.ServiceParameters {
64 | return &config.ServiceParameters{
65 | ConfigFlag: true,
66 | ServiceConfigFile: loadedFile,
67 | Service: &yml.Service,
68 | DB: &yml.DB,
69 | BatchWriter: &yml.BatchWriter,
70 | Redis: &yml.Redis,
71 | Osquery: &yml.Osquery,
72 | Osctrld: &yml.Osctrld,
73 | Metrics: &yml.Metrics,
74 | TLS: &yml.TLS,
75 | Logger: &yml.Logger,
76 | Carver: &yml.Carver,
77 | Debug: &yml.Debug,
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/enrolls.js:
--------------------------------------------------------------------------------
1 | function genericLinkAction(_type, _action) {
2 | var _csrftoken = $("#csrftoken").val();
3 | var _url = "/expiration/" + window.location.pathname.split("/").pop();
4 | var data = {
5 | csrftoken: _csrftoken,
6 | type: _type,
7 | action: _action,
8 | };
9 | sendPostRequest(data, _url, window.location.pathname, false);
10 | }
11 |
12 | function extendEnrollLink() {
13 | genericLinkAction("enroll", "extend");
14 | }
15 |
16 | function expireEnrollLink() {
17 | genericLinkAction("enroll", "expire");
18 | }
19 |
20 | function rotateEnrollLink() {
21 | genericLinkAction("enroll", "rotate");
22 | }
23 |
24 | function notexpireEnrollLink() {
25 | genericLinkAction("enroll", "notexpire");
26 | }
27 |
28 | function extendRemoveLink() {
29 | genericLinkAction("remove", "extend");
30 | }
31 |
32 | function expireRemoveLink() {
33 | genericLinkAction("remove", "expire");
34 | }
35 |
36 | function rotateRemoveLink() {
37 | genericLinkAction("remove", "rotate");
38 | }
39 |
40 | function notexpireRemoveLink() {
41 | genericLinkAction("remove", "notexpire");
42 | }
43 |
44 | function confirmUploadCertificate() {
45 | $("#certificate_action").click(function () {
46 | $("#certificateModal").modal("hide");
47 | uploadCertificate();
48 | });
49 | $("#certificateModal").modal();
50 | }
51 |
52 | function uploadCertificate() {
53 | var _csrftoken = $("#csrftoken").val();
54 | var _blob = $("#certificate").data("CodeMirrorInstance");
55 | var _certificate = _blob.getValue();
56 |
57 | var _url = window.location.pathname;
58 |
59 | var data = {
60 | csrftoken: _csrftoken,
61 | action: "enroll_certificate",
62 | certificate: btoa(_certificate),
63 | };
64 | sendPostRequest(data, _url, window.location.pathname, false);
65 | }
66 |
67 | function saveDebPackage() {
68 | var _package = $("#deb-package-value").val();
69 | savePackage(_package, "package_deb");
70 | }
71 |
72 | function saveRpmPackage() {
73 | var _package = $("#rpm-package-value").val();
74 | savePackage(_package, "package_rpm");
75 | }
76 |
77 | function savePkgPackage() {
78 | var _package = $("#pkg-package-value").val();
79 | savePackage(_package, "package_pkg");
80 | }
81 |
82 | function saveMsiPackage() {
83 | var _package = $("#msi-package-value").val();
84 | savePackage(_package, "package_msi");
85 | }
86 |
87 | function savePackage(_package, _action) {
88 | var _csrftoken = $("#csrftoken").val();
89 |
90 | var _url = window.location.pathname;
91 |
92 | var data = {
93 | csrftoken: _csrftoken,
94 | action: _action,
95 | packageurl: _package,
96 | };
97 | sendPostRequest(data, _url, window.location.pathname, false);
98 | }
99 |
--------------------------------------------------------------------------------
/deploy/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | # User to run nginx as. It changes based on the host.
2 | # Ubuntu: www-data
3 | # CentOS: nginx
4 | # Docker: nginx
5 | user SERVER_USER;
6 |
7 | worker_processes auto;
8 | pid /run/nginx.pid;
9 |
10 | # Load dynamic modules. See /usr/share/nginx/README.dynamic.
11 | # Ubuntu: /etc/nginx/modules-enabled/*.conf;
12 | # CentOS: /usr/share/nginx/modules/*.conf;
13 | include MODULES_CONF;
14 |
15 | events {
16 | worker_connections 1024;
17 | }
18 |
19 | http {
20 | ##
21 | # Logging Settings
22 | ##
23 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
24 | '$status $body_bytes_sent "$http_referer" '
25 | '"$http_user_agent" "$http_x_forwarded_for"';
26 |
27 | access_log /var/log/nginx/access.log;
28 | error_log /var/log/nginx/error.log;
29 |
30 | ##
31 | # Performance Settings
32 | ##
33 |
34 | # copies data between one FD and other from within the kernel
35 | # faster than read() + write()
36 | sendfile on;
37 |
38 | # send headers in one piece, it is better than sending them one by one
39 | tcp_nopush on;
40 |
41 | # don't buffer data sent, good for small data bursts in real time
42 | tcp_nodelay on;
43 |
44 | # allow the server to close connection on non responding client, this will free up memory
45 | reset_timedout_connection on;
46 |
47 | # request timed out -- default 60
48 | client_body_timeout 10;
49 |
50 | # if client stop responding, free up memory -- default 60
51 | send_timeout 2;
52 |
53 | # server will close connection after this time -- default 75
54 | keepalive_timeout 30;
55 |
56 | ##
57 | # Random Settings
58 | ##
59 | include /etc/nginx/mime.types;
60 | default_type application/octet-stream;
61 |
62 | # Limit size of request body
63 | client_max_body_size 20M;
64 |
65 | # Do not send nginx version number in error pages or server header
66 | server_tokens off;
67 | server_name_in_redirect off;
68 |
69 | # Passive protections
70 | add_header X-XSS-Protection "1; mode=block";
71 | add_header X-Content-Type-Options nosniff;
72 |
73 | # CSP headers
74 | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self'; frame-src 'self'; object-src 'none'";
75 |
76 | # Listen to 80, redirect to HTTPS
77 | server {
78 | listen 80;
79 | rewrite ^ https://$host$request_uri? permanent;
80 | }
81 |
82 | ##
83 | # http://nginx.org/en/docs/ngx_core_module.html#include
84 | ##
85 | include /etc/nginx/conf.d/*.conf;
86 | include /etc/nginx/sites-enabled/*;
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/logging/file.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/jmpsec/osctrl/pkg/config"
5 | "github.com/jmpsec/osctrl/pkg/settings"
6 | "github.com/jmpsec/osctrl/pkg/types"
7 | "github.com/rs/zerolog"
8 | "github.com/rs/zerolog/log"
9 | lumberjack "gopkg.in/natefinch/lumberjack.v2"
10 | )
11 |
12 | // LoggerFile will be used to log data using external file
13 | type LoggerFile struct {
14 | Enabled bool
15 | Filename string
16 | Logger *zerolog.Logger
17 | }
18 |
19 | // CreateLoggerFile to initialize the logger
20 | func CreateLoggerFile(cfg *config.LocalLogger) (*LoggerFile, error) {
21 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
22 | z := zerolog.New(&lumberjack.Logger{
23 | Filename: cfg.FilePath,
24 | MaxSize: cfg.MaxSize,
25 | MaxBackups: cfg.MaxBackups,
26 | MaxAge: cfg.MaxAge,
27 | Compress: cfg.Compress,
28 | })
29 | logger := z.With().Caller().Timestamp().Logger()
30 | return &LoggerFile{
31 | Enabled: true,
32 | Filename: cfg.FilePath,
33 | Logger: &logger,
34 | }, nil
35 | }
36 |
37 | // Settings - Function to prepare settings for the logger
38 | func (logFile *LoggerFile) Settings(mgr *settings.Settings) {
39 | log.Info().Msg("No file logging settings")
40 | }
41 |
42 | // Log - Function that sends JSON result/status/query logs to stdout
43 | func (logFile *LoggerFile) Log(logType string, data []byte, environment, uuid string, debug bool) {
44 | if debug {
45 | log.Debug().Msgf("Sending %d bytes to stdout for %s - %s", len(data), environment, uuid)
46 | }
47 | switch logType {
48 | case types.StatusLog:
49 | logFile.Status(data, environment, uuid, debug)
50 | case types.ResultLog:
51 | logFile.Result(data, environment, uuid, debug)
52 | }
53 | }
54 |
55 | // Status - Function that sends JSON status logs to stdout
56 | func (logFile *LoggerFile) Status(data []byte, environment, uuid string, debug bool) {
57 | logFile.Logger.Info().Str(
58 | "type", types.StatusLog).Str(
59 | "environment", environment).Str(
60 | "uuid", uuid).RawJSON("data", data)
61 | }
62 |
63 | // Result - Function that sends JSON result logs to stdout
64 | func (logFile *LoggerFile) Result(data []byte, environment, uuid string, debug bool) {
65 | logFile.Logger.Info().Str(
66 | "type", types.ResultLog).Str(
67 | "environment", environment).Str(
68 | "uuid", uuid).RawJSON("data", data)
69 | }
70 |
71 | // Query - Function that sends JSON query logs to stdout
72 | func (logFile *LoggerFile) Query(data []byte, environment, uuid, name string, status int, debug bool) {
73 | logFile.Logger.Info().Str(
74 | "type", types.QueryLog).Str(
75 | "environment", environment).Str(
76 | "name", name).Int(
77 | "status", status).Str(
78 | "uuid", uuid).RawJSON("data", data)
79 | }
80 |
--------------------------------------------------------------------------------
/deploy/docker/conf/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | # User to run nginx as. It changes based on the host.
2 | # Ubuntu: www-data
3 | # CentOS: nginx
4 | # Docker: nginx
5 | user nginx;
6 |
7 | worker_processes auto;
8 | pid /run/nginx.pid;
9 |
10 | # Load dynamic modules. See /usr/share/nginx/README.dynamic.
11 | # Ubuntu: /etc/nginx/modules-enabled/*.conf;
12 | # CentOS: /usr/share/nginx/modules/*.conf;
13 | include /usr/share/nginx/modules/*.conf;
14 |
15 | events {
16 | worker_connections 1024;
17 | }
18 |
19 | http {
20 | ##
21 | # Logging Settings
22 | ##
23 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
24 | '$status $body_bytes_sent "$http_referer" '
25 | '"$http_user_agent" "$http_x_forwarded_for"';
26 |
27 | access_log /var/log/nginx/access.log;
28 | error_log /var/log/nginx/error.log;
29 |
30 | ##
31 | # Performance Settings
32 | ##
33 |
34 | # copies data between one FD and other from within the kernel
35 | # faster than read() + write()
36 | sendfile on;
37 |
38 | # send headers in one piece, it is better than sending them one by one
39 | tcp_nopush on;
40 |
41 | # don't buffer data sent, good for small data bursts in real time
42 | tcp_nodelay on;
43 |
44 | # allow the server to close connection on non responding client, this will free up memory
45 | reset_timedout_connection on;
46 |
47 | # request timed out -- default 60
48 | client_body_timeout 10;
49 |
50 | # if client stop responding, free up memory -- default 60
51 | send_timeout 2;
52 |
53 | # server will close connection after this time -- default 75
54 | keepalive_timeout 30;
55 |
56 | ##
57 | # Random Settings
58 | ##
59 | include /etc/nginx/mime.types;
60 | default_type application/octet-stream;
61 |
62 | # Limit size of request body
63 | client_max_body_size 20M;
64 |
65 | # Do not send nginx version number in error pages or server header
66 | server_tokens off;
67 | server_name_in_redirect off;
68 |
69 | # Passive protections
70 | add_header X-XSS-Protection "1; mode=block";
71 | add_header X-Content-Type-Options nosniff;
72 |
73 | # CSP headers
74 | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self'; frame-src 'self'; object-src 'none'";
75 |
76 | # Listen to 80, redirect to HTTPS
77 | server {
78 | listen 80;
79 | rewrite ^ https://$host$request_uri? permanent;
80 | }
81 |
82 | ##
83 | # http://nginx.org/en/docs/ngx_core_module.html#include
84 | ##
85 | include /etc/nginx/conf.d/*.conf;
86 | include /etc/nginx/sites-enabled/*;
87 | }
88 |
--------------------------------------------------------------------------------
/cmd/admin/handlers/json-audit.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/jmpsec/osctrl/cmd/admin/sessions"
8 | "github.com/jmpsec/osctrl/pkg/environments"
9 | "github.com/jmpsec/osctrl/pkg/users"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | // AuditLogJSON to be used to populate JSON data for audit logs
15 | type AuditLogJSON struct {
16 | Service string `json:"service"`
17 | Username string `json:"username"`
18 | Line string `json:"line"`
19 | SourceIP string `json:"sourceip"`
20 | LogType string `json:"logtype"`
21 | Severity string `json:"severity"`
22 | Env string `json:"environment"`
23 | When CreationTimes `json:"when"`
24 | }
25 |
26 | // ReturnedAudit to return a JSON with audit logs
27 | type ReturnedAudit struct {
28 | Data []AuditLogJSON `json:"data"`
29 | }
30 |
31 | // JSONAuditLogHandler for audit logs in JSON
32 | func (h *HandlersAdmin) JSONAuditLogHandler(w http.ResponseWriter, r *http.Request) {
33 | if h.DebugHTTPConfig.EnableHTTP {
34 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
35 | }
36 | // Get context data
37 | ctx := r.Context().Value(sessions.ContextKey(sessions.CtxSession)).(sessions.ContextValue)
38 | // Check permissions
39 | if !h.Users.CheckPermissions(ctx[sessions.CtxUser], users.AdminLevel, users.NoEnvironment) {
40 | adminErrorResponse(w, fmt.Sprintf("%s has insufficient permissions", ctx[sessions.CtxUser]), http.StatusForbidden, nil)
41 | return
42 | }
43 | // Get all environments
44 | envs, err := h.Envs.All()
45 | if err != nil {
46 | log.Err(err).Msg("error getting environments")
47 | return
48 | }
49 | // Get audit logs
50 | auditLogs, err := h.AuditLog.GetAll()
51 | if err != nil {
52 | log.Err(err).Msg("error getting audit logs")
53 | return
54 | }
55 | // Prepare data to be returned
56 | var auditLogsJSON []AuditLogJSON
57 | for _, logEntry := range auditLogs {
58 | auditLogsJSON = append(auditLogsJSON, AuditLogJSON{
59 | Service: logEntry.Service,
60 | Username: logEntry.Username,
61 | Line: logEntry.Line,
62 | SourceIP: logEntry.SourceIP,
63 | LogType: h.AuditLog.LogTypeToString(logEntry.LogType),
64 | Severity: h.AuditLog.SeverityToString(logEntry.Severity),
65 | Env: environments.EnvironmentFinderID(logEntry.EnvironmentID, envs, false),
66 | When: CreationTimes{
67 | Display: utils.PastFutureTimes(logEntry.CreatedAt),
68 | // Use Unix timestamp in seconds
69 | Timestamp: utils.TimeTimestamp(logEntry.CreatedAt),
70 | },
71 | })
72 | }
73 | returned := ReturnedAudit{
74 | Data: auditLogsJSON,
75 | }
76 | // Serve JSON
77 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, returned)
78 | }
79 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build a release with GoReleaser
2 |
3 | on:
4 | push:
5 | # Publish semver tags as releases.
6 | tags: ["v*.*.*"]
7 |
8 | permissions:
9 | contents: write
10 | packages: write
11 |
12 | env:
13 | GOLANG_VERSION: 1.25.4
14 |
15 | jobs:
16 | goreleaser:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
21 | with:
22 | fetch-depth: 0
23 |
24 | - name: Set up Go
25 | uses: actions/setup-go@8e57b58e57be52ac95949151e2777ffda8501267 # v5.5.0
26 | with:
27 | go-version: ${{ env.GOLANG_VERSION }}
28 |
29 | - name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@af1b253b8dc984466d22633f04ef341c1520ed2f # v3.11.1
31 |
32 | - name: Log in to Docker Hub
33 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
34 | with:
35 | username: ${{ secrets.DOCKER_HUB_USERNAME }}
36 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
37 |
38 | - name: Run GoReleaser
39 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
40 | with:
41 | distribution: goreleaser
42 | version: latest
43 | args: release --snapshot --skip-publish --clean
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | DOCKER_HUB_ORG: ${{ secrets.DOCKER_HUB_ORG }}
47 |
48 | # Optional: Sign Docker images with cosign
49 | sign:
50 | needs: goreleaser
51 | runs-on: ubuntu-latest
52 | if: startsWith(github.ref, 'refs/tags/')
53 | steps:
54 | - name: Checkout
55 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
56 |
57 | - name: Install cosign
58 | uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
59 |
60 | - name: Log in to Docker Hub
61 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
62 | with:
63 | username: ${{ secrets.DOCKER_HUB_USERNAME }}
64 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
65 |
66 | - name: Sign Docker images
67 | run: |
68 | for component in tls admin api cli; do
69 | cosign sign --yes docker.io/${{ secrets.DOCKER_HUB_ORG }}/osctrl-$component:${{ github.ref_name }}
70 | cosign verify \
71 | --certificate-identity-regexp="https://github.com/${{ github.repository }}/.github/workflows/.*" \
72 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
73 | docker.io/${{ secrets.DOCKER_HUB_ORG }}/osctrl-$component:${{ github.ref_name }}
74 | done
75 |
--------------------------------------------------------------------------------
/cmd/api/handlers/get.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/jmpsec/osctrl/pkg/users"
8 | "github.com/jmpsec/osctrl/pkg/utils"
9 | )
10 |
11 | // HealthHandler - Handle health requests
12 | func (h *HandlersApi) HealthHandler(w http.ResponseWriter, r *http.Request) {
13 | // Debug HTTP if enabled
14 | if h.DebugHTTPConfig.EnableHTTP {
15 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
16 | }
17 | // Send response
18 | utils.HTTPResponse(w, "", http.StatusOK, []byte(okContent))
19 | }
20 |
21 | // CheckHandlerNoAuth - Handle unauthenticated check requests
22 | func (h *HandlersApi) CheckHandlerNoAuth(w http.ResponseWriter, r *http.Request) {
23 | // Debug HTTP if enabled
24 | if h.DebugHTTPConfig.EnableHTTP {
25 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
26 | }
27 | // Send response
28 | utils.HTTPResponse(w, "Checked", http.StatusOK, []byte(okContent))
29 | }
30 |
31 | // CheckHandlerAuth - Handle authenticated check requests
32 | func (h *HandlersApi) CheckHandlerAuth(w http.ResponseWriter, r *http.Request) {
33 | // Debug HTTP if enabled
34 | if h.DebugHTTPConfig.EnableHTTP {
35 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
36 | }
37 | // Get context data and check access
38 | ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue)
39 | if !h.Users.CheckPermissions(ctx[ctxUser], users.UserLevel, users.NoEnvironment) {
40 | apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser]))
41 | return
42 | }
43 | // Send response
44 | utils.HTTPResponse(w, "Checked", http.StatusOK, []byte(okContent))
45 | }
46 |
47 | // RootHandler - Handle root requests
48 | func (h *HandlersApi) RootHandler(w http.ResponseWriter, r *http.Request) {
49 | // Debug HTTP if enabled
50 | if h.DebugHTTPConfig.EnableHTTP {
51 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
52 | }
53 | // Send response
54 | utils.HTTPResponse(w, "", http.StatusOK, []byte(okContent))
55 | }
56 |
57 | // ErrorHandler - Handle error requests
58 | func (h *HandlersApi) ErrorHandler(w http.ResponseWriter, r *http.Request) {
59 | // Debug HTTP if enabled
60 | if h.DebugHTTPConfig.EnableHTTP {
61 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
62 | }
63 | // Send response
64 | utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(errorContent))
65 | }
66 |
67 | // ForbiddenHandler - Handle forbidden error requests
68 | func (h *HandlersApi) ForbiddenHandler(w http.ResponseWriter, r *http.Request) {
69 | // Debug HTTP if enabled
70 | if h.DebugHTTPConfig.EnableHTTP {
71 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
72 | }
73 | // Send response
74 | utils.HTTPResponse(w, "", http.StatusForbidden, []byte(errorContent))
75 | }
76 |
--------------------------------------------------------------------------------
/cmd/admin/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ template "page-head" . }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
Login
16 |
get access to {{ .Project }}
17 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
63 |
64 |
65 |
66 |
67 | {{ template "page-js" . }}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/.github/actions/tagged_release/docker/codesign/action.yml:
--------------------------------------------------------------------------------
1 | name: "Sign osctrl Docker images"
2 | description: "Sign osctrl Docker images"
3 | inputs:
4 | osctrl_component:
5 | required: true
6 | description: Define the osctrl component to compile
7 | docker_tags:
8 | required: true
9 | description: Define the Docker tag
10 | docker_image_digests:
11 | required: true
12 | description: Dockerhub image digest
13 | docker_hub_org:
14 | required: true
15 | description: Pass DockerHub org to action
16 | docker_hub_username:
17 | required: true
18 | description: Pass DockerHub username to action
19 | docker_hub_access_token:
20 | required: true
21 | description: Pass DockerHub access token to action
22 |
23 | runs:
24 | using: "composite"
25 | steps:
26 | ########################### Checkout code ###########################
27 | - name: Checkout code
28 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
29 | with:
30 | fetch-depth: 2
31 |
32 | ########################### Install cosign ###########################
33 | # https://github.com/sigstore/cosign-installer
34 | - name: Install cosign
35 | uses: sigstore/cosign-installer@v3.9.2
36 |
37 | ########################### Log into Dockerhub ###########################
38 | - name: Login to Docker Hub
39 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
40 | with:
41 | username: ${{ inputs.docker_hub_username }}
42 | password: ${{ inputs.docker_hub_access_token }}
43 |
44 | ########################### Sign built Docker image using keyless cosign ###########################
45 | - name: Sign the published Docker image
46 | shell: bash
47 | run: |
48 | # Parse space-separated digests
49 | IFS=' ' read -ra DIGESTS <<< "${{ inputs.docker_image_digests }}"
50 |
51 | for digest in "${DIGESTS[@]}"
52 | do
53 | IMAGE_NAME="${{ inputs.docker_hub_org }}/osctrl-${{ inputs.osctrl_component }}"
54 | cosign sign --yes docker.io/$IMAGE_NAME@sha256:$digest
55 | done
56 |
57 | ########################### Verify signed image using keyless cosign ###########################
58 | - name: Verify the signed published Docker image
59 | shell: bash
60 | run: |
61 | # Parse space-separated digests
62 | IFS=' ' read -ra DIGESTS <<< "${{ inputs.docker_image_digests }}"
63 |
64 | for digest in "${DIGESTS[@]}"
65 | do
66 | IMAGE_NAME="${{ inputs.docker_hub_org }}/osctrl-${{ inputs.osctrl_component }}"
67 | cosign verify \
68 | --certificate-identity-regexp="https://github.com/${{ github.repository }}/.github/workflows/.*" \
69 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
70 | docker.io/$IMAGE_NAME@sha256:$digest
71 | done
72 |
--------------------------------------------------------------------------------
/cmd/admin/static/js/profile.js:
--------------------------------------------------------------------------------
1 | function profileChangePassword(_username) {
2 | $("#new_password").val('');
3 | $("#confirm_password").val('');
4 | $("#change_password_username").val(_username);
5 | $("#change_password_header").text('Change Password for ' + _username);
6 | $("#changePasswordModal").modal();
7 | }
8 |
9 | function profileConfirmChangePassword() {
10 | var _csrftoken = $("#csrftoken").val();
11 |
12 | var _url = window.location.pathname;
13 |
14 | var _username = $("#change_password_username").val();
15 | var _newpassword = $("#new_password").val();
16 | var _oldpassword = $("#old_password").val();
17 |
18 | var data = {
19 | csrftoken: _csrftoken,
20 | action: 'change_password',
21 | username: _username,
22 | new_password: _newpassword,
23 | old_password: _oldpassword,
24 | };
25 | sendPostRequest(data, _url, _url, false);
26 | }
27 |
28 | function profileEditSave() {
29 | var _csrftoken = $("#csrftoken").val();
30 |
31 | var _url = window.location.pathname;
32 |
33 | var _username = $("#profile_username").val();
34 | var _email = $("#profile_email").val();
35 | var _fullname = $("#profile_fullname").val();
36 |
37 | var data = {
38 | csrftoken: _csrftoken,
39 | action: 'edit',
40 | username: _username,
41 | email: _email,
42 | fullname: _fullname,
43 | };
44 | sendPostRequest(data, _url, '', true);
45 | }
46 |
47 | function toggleAPIToken() {
48 | var tokenField = document.getElementById("profile_api_token");
49 | var tokenValue = document.getElementById("profile_api_token_value").value;
50 | if (tokenField.type === "password") {
51 | tokenField.type = "text";
52 | tokenField.value = tokenValue;
53 | $("#button-eye").html('');
54 | } else {
55 | tokenField.type = "password";
56 | tokenField.value = "••••••••••••••••••••••••••••••";
57 | $("#button-eye").html('');
58 | }
59 | }
60 |
61 | function refreshUserToken() {
62 | $("#refreshTokenButton").prop("disabled", true);
63 | $("#refreshTokenButton").html(
64 | ''
65 | );
66 | var _csrftoken = $("#csrftoken").val();
67 | var _username = $("#profile_token_username").val();
68 | var _exp_hours = parseInt($("#profile_exp_hours").val());
69 | var data = {
70 | csrftoken: _csrftoken,
71 | username: _username,
72 | exp_hours: _exp_hours,
73 | };
74 | sendPostRequest(
75 | data,
76 | "/tokens/" + _username + "/refresh",
77 | "",
78 | false,
79 | function (data) {
80 | console.log(data);
81 | $("#profile_api_token_value").val(data.token);
82 | var expDiv = document.getElementById('profile_token_exp');
83 | expDiv.innerText = data.expiration;
84 | $("#refreshTokenButton").prop("disabled", false);
85 | $("#refreshTokenButton").html('');
86 | }
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/logging/kinesis.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/jmpsec/osctrl/pkg/config"
8 | "github.com/jmpsec/osctrl/pkg/settings"
9 | "github.com/rs/zerolog/log"
10 |
11 | "github.com/aws/aws-sdk-go-v2/aws"
12 | awsconfig "github.com/aws/aws-sdk-go-v2/config"
13 | "github.com/aws/aws-sdk-go-v2/credentials"
14 | "github.com/aws/aws-sdk-go-v2/service/kinesis"
15 | )
16 |
17 | // LoggerKinesis will be used to log data using Kinesis
18 | type LoggerKinesis struct {
19 | Configuration config.KinesisLogger
20 | KinesisClient *kinesis.Client
21 | Enabled bool
22 | }
23 |
24 | // CreateLoggerKinesis to initialize the logger
25 | func CreateLoggerKinesis(cfg *config.KinesisLogger) (*LoggerKinesis, error) {
26 | loadOpts := []func(*awsconfig.LoadOptions) error{
27 | awsconfig.WithRegion(cfg.Region),
28 | }
29 | if cfg.AccessKeyID != "" || cfg.SecretAccessKey != "" || cfg.SessionToken != "" {
30 | loadOpts = append(loadOpts,
31 | awsconfig.WithCredentialsProvider(
32 | credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, cfg.SessionToken),
33 | ))
34 | }
35 |
36 | awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(), loadOpts...)
37 | if err != nil {
38 | return nil, fmt.Errorf("load AWS config: %w", err)
39 | }
40 |
41 | var kinesisOpts []func(*kinesis.Options)
42 | if cfg.Endpoint != "" {
43 | endpoint := cfg.Endpoint
44 | kinesisOpts = append(kinesisOpts, func(o *kinesis.Options) {
45 | o.BaseEndpoint = aws.String(endpoint)
46 | })
47 | }
48 |
49 | kc := kinesis.NewFromConfig(awsCfg, kinesisOpts...)
50 |
51 | if _, err := kc.DescribeStream(context.Background(), &kinesis.DescribeStreamInput{
52 | StreamName: aws.String(cfg.Stream),
53 | }); err != nil {
54 | return nil, fmt.Errorf("DescribeStream: %w", err)
55 | }
56 |
57 | return &LoggerKinesis{
58 | Configuration: *cfg,
59 | KinesisClient: kc,
60 | Enabled: true,
61 | }, nil
62 | }
63 |
64 | // Settings - Function to prepare settings for the logger
65 | func (logSK *LoggerKinesis) Settings(mgr *settings.Settings) {
66 | log.Info().Msg("No kinesis logging settings")
67 | }
68 |
69 | // Send - Function that sends JSON logs to Splunk HTTP Event Collector
70 | func (logSK *LoggerKinesis) Send(logType string, data []byte, environment, uuid string, debug bool) {
71 | if debug {
72 | log.Debug().Msgf("Sending %d bytes to Kinesis for %s - %s", len(data), environment, uuid)
73 | }
74 | streamName := aws.String(logSK.Configuration.Stream)
75 | putOutput, err := logSK.KinesisClient.PutRecord(context.Background(), &kinesis.PutRecordInput{
76 | Data: data,
77 | StreamName: streamName,
78 | PartitionKey: aws.String(logType + ":" + environment + ":" + uuid),
79 | })
80 | if err != nil {
81 | log.Err(err).Msg("Error sending kinesis stream")
82 | return
83 | }
84 | if debug {
85 | log.Debug().Msgf("PutRecordOutput %+v", putOutput)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/cmd/api/handlers/platforms.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/jmpsec/osctrl/pkg/auditlog"
9 | "github.com/jmpsec/osctrl/pkg/users"
10 | "github.com/jmpsec/osctrl/pkg/utils"
11 | "github.com/rs/zerolog/log"
12 | )
13 |
14 | // PlatformsHandler - GET Handler for multiple JSON platforms
15 | func (h *HandlersApi) PlatformsHandler(w http.ResponseWriter, r *http.Request) {
16 | // Debug HTTP if enabled
17 | if h.DebugHTTPConfig.EnableHTTP {
18 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
19 | }
20 | // Get context data and check access
21 | ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue)
22 | if !h.Users.CheckPermissions(ctx[ctxUser], users.AdminLevel, users.NoEnvironment) {
23 | apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser]))
24 | return
25 | }
26 | // Get platforms
27 | platforms, err := h.Nodes.GetAllPlatforms()
28 | if err != nil {
29 | apiErrorResponse(w, "error getting platforms", http.StatusInternalServerError, err)
30 | return
31 | }
32 | // Serialize and serve JSON
33 | log.Debug().Msg("Returned platforms")
34 | h.AuditLog.Visit(ctx[ctxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment)
35 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, platforms)
36 | }
37 |
38 | // PlatformsEnvHandler - GET Handler to return platforms for one environment as JSON
39 | func (h *HandlersApi) PlatformsEnvHandler(w http.ResponseWriter, r *http.Request) {
40 | // Debug HTTP if enabled
41 | if h.DebugHTTPConfig.EnableHTTP {
42 | utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
43 | }
44 | // Extract environment
45 | envVar := r.PathValue("env")
46 | if envVar == "" {
47 | apiErrorResponse(w, "error getting environment", http.StatusBadRequest, nil)
48 | return
49 | }
50 | // Get environment by name
51 | env, err := h.Envs.GetByUUID(envVar)
52 | if err != nil {
53 | if err.Error() == "record not found" {
54 | apiErrorResponse(w, "environment not found", http.StatusNotFound, err)
55 | } else {
56 | apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, err)
57 | }
58 | return
59 | }
60 | // Get context data and check access
61 | ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue)
62 | if !h.Users.CheckPermissions(ctx[ctxUser], users.AdminLevel, users.NoEnvironment) {
63 | apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser]))
64 | return
65 | }
66 | // Get platforms
67 | platforms, err := h.Nodes.GetEnvPlatforms(env.UUID)
68 | if err != nil {
69 | apiErrorResponse(w, "error getting platforms", http.StatusInternalServerError, err)
70 | return
71 | }
72 | // Serialize and serve JSON
73 | log.Debug().Msg("Returned platforms")
74 | h.AuditLog.Visit(ctx[ctxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID)
75 | utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, platforms)
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/utils/time-utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "strconv"
5 | "time"
6 | )
7 |
8 | var (
9 | // OneMinute - 60 seconds
10 | OneMinute = 60 * time.Second
11 | // OneHour - 3600 seconds
12 | OneHour = 60 * time.Minute
13 | // SixHours - 21600 seconds
14 | SixHours = 6 * time.Hour
15 | // OneDay - 86400 seconds
16 | OneDay = 24 * time.Hour
17 | // FifteenDays - 1296000 seconds
18 | FifteenDays = 15 * OneDay
19 | )
20 |
21 | // StringifyTime - Helper to get a string based on the difference of two times
22 | func StringifyTime(seconds int) string {
23 | var timeStr string
24 | w := make(map[int]string)
25 | w[DurationSeconds(OneDay)] = "day"
26 | w[DurationSeconds(OneHour)] = "hour"
27 | w[DurationSeconds(OneMinute)] = "minute"
28 | // Ordering the values will prevent bad values
29 | ww := [3]int{DurationSeconds(OneDay), DurationSeconds(OneHour), DurationSeconds(OneMinute)}
30 | for _, v := range ww {
31 | if seconds >= v {
32 | d := seconds / v
33 | dStr := strconv.Itoa(d)
34 | timeStr = dStr + " " + w[v]
35 | if d > 1 {
36 | timeStr += "s"
37 | }
38 | break
39 | }
40 | }
41 | return timeStr
42 | }
43 |
44 | // DurationSeconds - Helper to get the seconds value fom a Duration
45 | func DurationSeconds(duration time.Duration) int {
46 | return int(duration.Seconds())
47 | }
48 |
49 | // TimeTimestamp - Helper to format times in timestamp format
50 | func TimeTimestamp(t time.Time) string {
51 | return strconv.FormatInt(t.Unix(), 10)
52 | }
53 |
54 | // PastFutureTimes - Helper to format past or future times
55 | func PastFutureTimes(t time.Time) string {
56 | if t.Before(time.Now()) {
57 | return PastTimeAgo(t)
58 | }
59 | return InFutureTime(t)
60 | }
61 |
62 | // PastFutureTimesEpoch - Helper to format past or future times
63 | func PastFutureTimesEpoch(ts int64) string {
64 | return PastFutureTimes(time.Unix(ts, 0))
65 | }
66 |
67 | // PastTimeAgo - Helper to format past times only returning one value (minute, hour, day)
68 | func PastTimeAgo(t time.Time) string {
69 | if t.IsZero() {
70 | return "Never"
71 | }
72 | now := time.Now()
73 | seconds := DurationSeconds(now.Sub(t))
74 | if seconds < 2 {
75 | return "Just Now"
76 | }
77 | if seconds < DurationSeconds(OneMinute) {
78 | return strconv.Itoa(seconds) + " seconds ago"
79 | }
80 | if seconds > DurationSeconds(FifteenDays) {
81 | return "Since " + t.Format("Mon Jan 02 15:04:05 MST 2006")
82 | }
83 | return StringifyTime(seconds) + " ago"
84 | }
85 |
86 | // InFutureTime - Helper to format future times only returning one value (minute, hour, day)
87 | func InFutureTime(t time.Time) string {
88 | if t.IsZero() {
89 | return "Never Expires"
90 | }
91 | now := time.Now()
92 | seconds := int(t.Sub(now).Seconds())
93 | if seconds <= 2 {
94 | return "Expired"
95 | }
96 | if seconds < DurationSeconds(OneMinute) {
97 | return "Expires in " + strconv.Itoa(seconds) + " seconds"
98 | }
99 | return "Expires in " + StringifyTime(seconds)
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/carves/utils.go:
--------------------------------------------------------------------------------
1 | package carves
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/jmpsec/osctrl/pkg/utils"
10 | )
11 |
12 | const (
13 | // S3proto to be used as s3 URL
14 | S3proto = "s3://"
15 | // S3URL to format the s3 URL
16 | S3URL = S3proto + "%s/%s"
17 | // S3Key to format the s3 key for a block
18 | S3Key = "%s:%s:%s:%d"
19 | // S3File to format the s3 key for a reconstructed file
20 | S3File = "%s:%s:%s:%s" + TarFileExtension
21 | // LocalFile to format the local file name
22 | LocalFile = "%s_%s_%s" + TarFileExtension
23 | )
24 |
25 | // Function to generate a carve block filename for s3
26 | func GenerateS3Data(bucket, env, uuid, sessionid string, blockid int) string {
27 | return fmt.Sprintf(S3URL, bucket, GenerateS3Key(env, uuid, sessionid, blockid))
28 | }
29 |
30 | // Function to generate a carve archived filename for s3
31 | func GenerateS3Archive(bucket, env, uuid, sessionid, path string) string {
32 | return fmt.Sprintf(S3URL, bucket, GenerateS3File(env, uuid, sessionid, path))
33 | }
34 |
35 | // Function to generate the s3 key for a carve block
36 | func GenerateS3Key(env, uuid, sessionid string, blockid int) string {
37 | return fmt.Sprintf(S3Key, env, uuid, sessionid, blockid)
38 | }
39 |
40 | // Function to generate the s3 file reconstructed from blocks
41 | func GenerateS3File(env, uuid, sessionid, path string) string {
42 | return fmt.Sprintf(S3File, env, uuid, sessionid, path)
43 | }
44 |
45 | // Function to translate from a s3:// URL to just the key
46 | func S3URLtoKey(s3url, bucket string) string {
47 | return strings.TrimPrefix(s3url, fmt.Sprintf(S3proto+"%s/", bucket))
48 | }
49 |
50 | // Function to generate a local file for carve archives
51 | func GenerateArchiveName(carve CarvedFile) string {
52 | cPath := strings.ReplaceAll(strings.ReplaceAll(carve.Path, "/", "-"), "\\", "-")
53 | return fmt.Sprintf(LocalFile, carve.UUID, carve.SessionID, cPath)
54 | }
55 |
56 | // Function to check if data is compressed using zstd
57 | // https://github.com/facebook/zstd
58 | func CheckCompressionRaw(data []byte) bool {
59 | return bytes.Equal(data[:4], CompressionHeader)
60 | }
61 |
62 | // Function to check if a block data is compressed using zstd
63 | // https://github.com/facebook/zstd
64 | func CheckCompressionBlock(block CarvedBlock) (bool, error) {
65 | // Make sure this is the block 0
66 | if block.BlockID != 0 {
67 | return false, fmt.Errorf("block_id is not 0 (%d)", block.BlockID)
68 | }
69 | compressionCheck, err := base64.StdEncoding.DecodeString(block.Data)
70 | if err != nil {
71 | return false, fmt.Errorf("error decoding block %w", err)
72 | }
73 | return CheckCompressionRaw(compressionCheck), nil
74 | }
75 |
76 | // Helper to generate a random carve name
77 | func GenCarveName() string {
78 | return "carve_" + utils.RandomForNames()
79 | }
80 |
81 | // Helper to generate the carve query
82 | func GenCarveQuery(file string, glob bool) string {
83 | if glob {
84 | return "SELECT * FROM carves WHERE carve=1 AND path LIKE '" + file + "';"
85 | }
86 | return "SELECT * FROM carves WHERE carve=1 AND path = '" + file + "';"
87 | }
88 |
--------------------------------------------------------------------------------
/.github/actions/tagged_release/github/action.yml:
--------------------------------------------------------------------------------
1 | name: "Release binaries and packages"
2 | description: "Release binaries and packages"
3 | inputs:
4 | go_os:
5 | required: true
6 | description: Define the OS to compile binary for - https://pkg.go.dev/internal/goos
7 | go_arch:
8 | required: true
9 | description: Define the architecture to compile binary for - https://pkg.go.dev/internal/goarch
10 | commit_sha:
11 | required: true
12 | description: Define the SHA1 git commit hash
13 | release_version_tag:
14 | required: true
15 | description: Define the release version
16 | osctrl_component:
17 | required: true
18 | description: Define the osctrl component to compile
19 |
20 | runs:
21 | using: "composite"
22 | steps:
23 | ########################### Download osctrl binary ###########################
24 | - name: Download osctrl binaries
25 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
26 | with:
27 | name: osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin
28 |
29 | - name: PWD
30 | shell: bash
31 | run: "pwd"
32 |
33 | - name: LS
34 | shell: bash
35 | run: "ls -la"
36 |
37 | - name: Copy linux or darwin binary
38 | if: ${{ inputs.go_os }} == 'linux' || ${{ inputs.go_os }} == 'darwin'
39 | shell: bash
40 | run: |
41 | cp \
42 | osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin \
43 | osctrl-${{ inputs.osctrl_component }}-${{ inputs.release_version_tag }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin
44 |
45 | - name: Copy windows binary
46 | if: ${{ inputs.go_os }} == 'windows'
47 | shell: bash
48 | run: |
49 | cp \
50 | osctrl-${{ inputs.osctrl_component }}-${{ inputs.commit_sha }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.bin \
51 | osctrl-${{ inputs.osctrl_component }}-${{ inputs.release_version_tag }}-${{ inputs.go_os }}-${{ inputs.go_arch }}.exe
52 |
53 | ########################### Download osctrl DEB package ###########################
54 | - name: List available artifacts
55 | if: ${{ inputs.go_os }} == 'linux'
56 | run: |
57 | echo "Looking for DEB package: osctrl-${{ inputs.osctrl_component }}_${{ inputs.release_version_tag }}_${{ inputs.go_arch }}.deb"
58 | echo "Release version tag: ${{ inputs.release_version_tag }}"
59 | echo "Component: ${{ inputs.osctrl_component }}"
60 | echo "Arch: ${{ inputs.go_arch }}"
61 | shell: bash
62 |
63 | - name: Download osctrl DEB package
64 | if: ${{ inputs.go_os }} == 'linux'
65 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
66 | with:
67 | name: osctrl-${{ inputs.osctrl_component }}_${{ inputs.release_version_tag }}_${{ inputs.go_arch }}.deb
68 |
69 | ########################### Release ###########################
70 | - name: Release
71 | uses: softprops/action-gh-release@f82d31e53e61a962573dd0c5fcd6b446ca78871f # v2.3.2
72 | if: startsWith(github.ref, 'refs/tags/')
73 | with:
74 | files: |
75 | osctrl-*.bin
76 | osctrl-*.exe
77 | osctrl-*.deb
78 |
--------------------------------------------------------------------------------