Skip to content

Commit

Permalink
Merge pull request #14 from aau-network-security/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
Mikkelhost authored Apr 18, 2024
2 parents 21a50e1 + 2756223 commit 0f99e22
Show file tree
Hide file tree
Showing 32 changed files with 1,876 additions and 424 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ casbin.md
.data
.logs
state/state.json
log.log
tmp
pid
4 changes: 3 additions & 1 deletion config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ host: localhost
port: 8080
listening-ip: "127.0.0.1"
state-path: /path/to/state/dir
vm-version: kali-v*-*-*

lab-expiry-duration: 1 # In minutes
lab-expiry-extension: 1 # In minutes
eventRetention: 30 #Amount of days before stopped events are removed including teams related to that event

auditLog:
directory: /path/to/log/dir
Expand All @@ -19,7 +21,7 @@ db-config:
db_name: haaukins
username: haaukins
password: haaukins
eventRetention: 30 #Amount of days before stopped events are removed including teams related to that event


exercise-service:
grpc: localhost
Expand Down
12 changes: 10 additions & 2 deletions database/initdb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
EOSQL

PGPASSWORD=$HAAUKINSDB_PASSWORD psql -v ON_ERROR_STOP=1 --username "$HAAUKINSDB_USER" --dbname "$HAAUKINSDB_NAME" <<-EOSQL
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS events (
id serial primary key,
tag varchar (255) NOT NULL,
Expand Down Expand Up @@ -59,14 +60,17 @@ PGPASSWORD=$HAAUKINSDB_PASSWORD psql -v ON_ERROR_STOP=1 --username "$HAAUKINSDB_
name varchar (255) NOT NULL,
owner_user varchar(255) NOT NULL,
owner_email varchar(255) NOT NULL,
lab_quota integer,
UNIQUE(name)
);
CREATE UNIQUE INDEX orgname_lower_index ON organizations (LOWER(name));
CREATE TABLE IF NOT EXISTS profiles (
id serial primary key,
name varchar (255) NOT NULL,
secret boolean NOT NULL,
secret boolean NOT NULL,
description text NOT NULL,
public boolean NOT NULL,
organization varchar(255) NOT NULL REFERENCES organizations (name) ON DELETE CASCADE
);
CREATE UNIQUE INDEX profilename_lower_index ON profiles (LOWER(name), LOWER(organization));
Expand All @@ -80,12 +84,14 @@ PGPASSWORD=$HAAUKINSDB_PASSWORD psql -v ON_ERROR_STOP=1 --username "$HAAUKINSDB_
CREATE UNIQUE INDEX profile_challenges_duplicate_index ON profile_challenges (tag, profile_id);
CREATE TABLE IF NOT EXISTS admin_users (
id serial primary key,
id serial primary key,
sid uuid NOT NULL DEFAULT uuid_generate_v4(),
username varchar (255) NOT NULL,
password varchar (255) NOT NULL,
full_name varchar (255) NOT NULL,
email varchar (255) NOT NULL,
role varchar (255) NOT NULL,
lab_quota integer,
organization varchar (255) NOT NULL REFERENCES organizations (name) ON DELETE CASCADE
);
CREATE UNIQUE INDEX username_lower_index ON Admin_users (LOWER(username));
Expand All @@ -111,6 +117,8 @@ PGPASSWORD=$HAAUKINSDB_PASSWORD psql -v ON_ERROR_STOP=1 --username "$HAAUKINSDB_
);
CREATE UNIQUE INDEX frontendname_lower_index ON frontends (LOWER(name));
-- Setting up an administrative account with password admin
INSERT INTO organizations (name, owner_user, owner_email) VALUES ('Admins', 'admin', '[email protected]');
INSERT INTO admin_users (username, password, full_name, email, role, organization) VALUES ('admin', '\$2a\$10\$uwUoW.w5OZKEa5/UJrYyM.fz9vjH3z1sGsZWXZ2Nmf0obL9OK80kC', 'Mikkel Høst Christiansen', '[email protected]', 'role::superadmin', 'Admins');
Expand Down
10 changes: 4 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ module github.com/aau-network-security/haaukins-daemon

go 1.18

replace github.com/aau-network-security/haaukins-agent => /home/mikkel/Desktop/haaukinsdev/haaukins-agent

replace github.com/aau-network-security/haaukins-exercises => /home/mikkel/Desktop/haaukinsdev/haaukins-exercises

require (
github.com/aau-network-security/haaukins-agent v0.0.1
github.com/aau-network-security/haaukins-agent v1.0.0
github.com/aau-network-security/haaukins-exercises v1.2.2
github.com/casbin/casbin/v2 v2.51.2
github.com/casbin/gorm-adapter/v3 v3.8.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.8.1
github.com/go-redis/redis v6.15.9+incompatible
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.6
Expand Down Expand Up @@ -44,7 +42,7 @@ require (
github.com/goccy/go-json v0.9.7 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
Expand Down
16 changes: 15 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/aau-network-security/haaukins-agent v1.0.0 h1:0qH7QVS3ucyGC/Kp7nLpRhGJbKE4RSlVOApwJTakVGY=
github.com/aau-network-security/haaukins-agent v1.0.0/go.mod h1:inKZN9z/AY+3LayMGuWFRKMA2wZyqrEtyu+XoK1ukBE=
github.com/aau-network-security/haaukins-exercises v1.2.2 h1:uuGISyG4JmjOgAf2ZLFhvsv2M+ziV5Rs2yE5gaonBj4=
github.com/aau-network-security/haaukins-exercises v1.2.2/go.mod h1:i8bPuRe8F1mv9HTfcOzsP8vxOwclCaG+fOlSxSkRTLA=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
Expand Down Expand Up @@ -98,6 +102,8 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
Expand All @@ -106,8 +112,9 @@ github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQ
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
Expand Down Expand Up @@ -206,6 +213,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down Expand Up @@ -315,6 +323,7 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
Expand Down Expand Up @@ -359,6 +368,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand All @@ -370,6 +380,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
Expand Down Expand Up @@ -454,8 +465,11 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
46 changes: 39 additions & 7 deletions internal/daemon/adminAgents.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"sync"
"time"

"github.com/aau-network-security/haaukins-daemon/internal/db"
Expand Down Expand Up @@ -71,7 +72,12 @@ func (d *daemon) newAgent(c *gin.Context) {
return
}

admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -118,6 +124,7 @@ func (d *daemon) newAgent(c *gin.Context) {

streamCtx, cancel := context.WithCancel(context.Background())
agentForPool := &Agent{
M: sync.RWMutex{},
Name: req.Name,
Url: req.Url,
Tls: req.Tls,
Expand Down Expand Up @@ -166,7 +173,12 @@ func (d *daemon) newAgent(c *gin.Context) {
func (d *daemon) getAgents(c *gin.Context) {
ctx := context.Background()

admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -235,7 +247,12 @@ func (d *daemon) deleteAgent(c *gin.Context) {

agentName := c.Param("agent")

admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -286,7 +303,12 @@ func (d *daemon) deleteAgent(c *gin.Context) {
func (d *daemon) reconnectAgent(c *gin.Context) {
ctx := context.Background()

admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -339,6 +361,7 @@ func (d *daemon) reconnectAgent(c *gin.Context) {

streamCtx, cancel := context.WithCancel(context.Background())
agentForPool := &Agent{
M: sync.RWMutex{},
Name: dbAgent.Name,
Url: dbAgent.Url,
Tls: dbAgent.Tls,
Expand Down Expand Up @@ -377,7 +400,12 @@ func (d *daemon) reconnectAgent(c *gin.Context) {
}

func (d *daemon) lockAgentState(c *gin.Context) {
admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -409,7 +437,12 @@ func (d *daemon) lockAgentState(c *gin.Context) {
}

func (d *daemon) unlockAgentState(c *gin.Context) {
admin := unpackAdminClaims(c)
admin, err := d.getUserFromGinContext(c)
if err != nil {
log.Error().Err(err).Msg("error getting user from gin context")
c.JSON(http.StatusInternalServerError, APIResponse{Status: "Internal Server Error"})
return
}
d.auditLogger.Info().
Time("UTC", time.Now().UTC()).
Str("AdminUser", admin.Username).
Expand Down Expand Up @@ -446,7 +479,6 @@ var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}

// TODO Find a better way to authenticate users than sending jwt as get query parameter
func (d *daemon) agentWebsocket(c *gin.Context) {
agentName := c.Param("agent")

Expand Down
Loading

0 comments on commit 0f99e22

Please sign in to comment.