@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
@@ -0,0 +1,50 @@
|
||||
# xo-server-audit [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> **TODO**
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-audit):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-audit
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
@@ -0,0 +1,39 @@
|
||||
# List of skipped methods
|
||||
blockedList = [
|
||||
'acl.get',
|
||||
'acl.getCurrentPermissions',
|
||||
'audit.checkIntegrity',
|
||||
'audit.generateFingerprint',
|
||||
'audit.getRecords',
|
||||
'backupNg.getAllJobs',
|
||||
'backupNg.getAllLogs',
|
||||
'cloud.getResourceCatalog',
|
||||
'cloudConfig.getAll',
|
||||
'group.getAll',
|
||||
'host.stats',
|
||||
'ipPool.getAll',
|
||||
'job.getAll',
|
||||
'log.get',
|
||||
'metadataBackup.getAllJobs',
|
||||
'plugin.get',
|
||||
'pool.listMissingPatches',
|
||||
'proxy.getAll',
|
||||
'remote.getAll',
|
||||
'remote.getAllInfo',
|
||||
'resourceSet.getAll',
|
||||
'role.getAll',
|
||||
'schedule.getAll',
|
||||
'server.getAll',
|
||||
'session.getUser',
|
||||
'sr.getUnhealthyVdiChainsLength',
|
||||
'sr.stats',
|
||||
'system.getMethodsInfo',
|
||||
'system.getServerTimezone',
|
||||
'system.getServerVersion',
|
||||
'user.getAll',
|
||||
'vm.stats',
|
||||
'xo.getAllObjects',
|
||||
'xoa.supportTunnel.getState',
|
||||
'xosan.checkSrCurrentState',
|
||||
'xosan.getVolumeInfo',
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "xo-server-audit",
|
||||
"version": "0.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Audit plugin for XO-Server",
|
||||
"keywords": [
|
||||
"audit",
|
||||
"log",
|
||||
"logs",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"xen-orchestra",
|
||||
"xen",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-audit",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "packages/xo-server-audit",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.0",
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/audit-core": "^0.0.0",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"app-conf": "^0.7.1",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"readable-stream": "^3.5.0",
|
||||
"xo-common": "^0.3.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import appConf from 'app-conf'
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import path from 'path'
|
||||
import { alteredAuditRecord, missingAuditRecord } from 'xo-common/api-errors'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
import {
|
||||
AlteredRecordError,
|
||||
AuditCore,
|
||||
MissingRecordError,
|
||||
NULL_ID,
|
||||
Storage,
|
||||
} from '@xen-orchestra/audit-core'
|
||||
|
||||
const log = createLogger('xo:xo-server-audit')
|
||||
|
||||
const LAST_ID = 'lastId'
|
||||
class Db extends Storage {
|
||||
constructor(db) {
|
||||
super()
|
||||
this._db = db
|
||||
}
|
||||
|
||||
async put(record) {
|
||||
await this._db.put(record.id, record)
|
||||
}
|
||||
|
||||
async del(id) {
|
||||
await this._db.del(id)
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this._db.get(id).catch(error => {
|
||||
if (!error.notFound) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setLastId(id) {
|
||||
await this._db.put(LAST_ID, id)
|
||||
}
|
||||
|
||||
getLastId() {
|
||||
return this.get(LAST_ID)
|
||||
}
|
||||
}
|
||||
|
||||
const NAMESPACE = 'audit'
|
||||
class AuditXoPlugin {
|
||||
constructor({ xo }) {
|
||||
this._cleaners = []
|
||||
this._xo = xo
|
||||
|
||||
this._auditCore = undefined
|
||||
this._blockedList = undefined
|
||||
this._storage = undefined
|
||||
}
|
||||
|
||||
async load() {
|
||||
const cleaners = this._cleaners
|
||||
|
||||
try {
|
||||
const storage = (this._storage = new Db(
|
||||
await this._xo.getStore(NAMESPACE)
|
||||
))
|
||||
this._auditCore = new AuditCore(storage)
|
||||
this._blockedList = (
|
||||
await appConf.load('xo-server-audit', {
|
||||
appDir: path.join(__dirname, '..'),
|
||||
})
|
||||
).blockedList
|
||||
|
||||
cleaners.push(() => {
|
||||
this._auditCore = undefined
|
||||
this._blockedList = undefined
|
||||
this._storage = undefined
|
||||
})
|
||||
} catch (error) {
|
||||
this._auditCore = undefined
|
||||
this._blockedList = undefined
|
||||
this._storage = undefined
|
||||
throw error
|
||||
}
|
||||
|
||||
this._addListener('xo:postCall', this._handleEvent.bind(this, 'apiCall'))
|
||||
this._addListener('xo:audit', this._handleEvent.bind(this))
|
||||
|
||||
const getRecords = this._getRecords.bind(this)
|
||||
getRecords.description = 'Get records from a passed record ID'
|
||||
getRecords.permission = 'admin'
|
||||
getRecords.params = {
|
||||
id: { type: 'string', optional: true },
|
||||
ndjson: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
const checkIntegrity = this._checkIntegrity.bind(this)
|
||||
checkIntegrity.description =
|
||||
'Check records integrity between oldest and newest'
|
||||
checkIntegrity.permission = 'admin'
|
||||
checkIntegrity.params = {
|
||||
newest: { type: 'string', optional: true },
|
||||
oldest: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
const generateFingerprint = this._generateFingerprint.bind(this)
|
||||
generateFingerprint.description =
|
||||
'Generate a fingerprint of the chain oldest-newest'
|
||||
generateFingerprint.permission = 'admin'
|
||||
generateFingerprint.params = {
|
||||
newest: { type: 'string', optional: true },
|
||||
oldest: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
cleaners.push(
|
||||
this._xo.addApiMethods({
|
||||
audit: {
|
||||
checkIntegrity,
|
||||
generateFingerprint,
|
||||
getRecords,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
unload() {
|
||||
this._cleaners.forEach(cleaner => cleaner())
|
||||
this._cleaners.length = 0
|
||||
}
|
||||
|
||||
_addListener(event, listener_) {
|
||||
const listener = async (...args) => {
|
||||
try {
|
||||
await listener_(...args)
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
}
|
||||
const xo = this._xo
|
||||
xo.on(event, listener)
|
||||
this._cleaners.push(() => xo.removeListener(event, listener))
|
||||
}
|
||||
|
||||
_handleEvent(event, { userId, userIp, userName, ...data }) {
|
||||
if (event !== 'apiCall' || this._blockedList.indexOf(data.method) === -1) {
|
||||
return this._auditCore.add(
|
||||
{
|
||||
userId,
|
||||
userIp,
|
||||
userName,
|
||||
},
|
||||
event,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_handleGetRecords(req, res, id) {
|
||||
res.set('Content-Type', 'application/json')
|
||||
return fromCallback(
|
||||
pipeline,
|
||||
asyncIteratorToStream(async function*(asyncIterator) {
|
||||
for await (const record of asyncIterator) {
|
||||
yield JSON.stringify(record)
|
||||
yield '\n'
|
||||
}
|
||||
})(this._auditCore.getFrom(id)),
|
||||
res
|
||||
)
|
||||
}
|
||||
|
||||
async _getRecords({ id, ndjson = false }) {
|
||||
if (ndjson) {
|
||||
return this._xo
|
||||
.registerHttpRequest(this._handleGetRecords.bind(this), id)
|
||||
.then($getFrom => ({
|
||||
$getFrom,
|
||||
}))
|
||||
}
|
||||
|
||||
const records = []
|
||||
for await (const record of this._auditCore.getFrom(id)) {
|
||||
records.push(record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
async _checkIntegrity(props) {
|
||||
const { oldest = NULL_ID, newest = await this._storage.getLastId() } = props
|
||||
return this._auditCore.checkIntegrity(oldest, newest).catch(error => {
|
||||
if (error instanceof MissingRecordError) {
|
||||
throw missingAuditRecord(error)
|
||||
}
|
||||
if (error instanceof AlteredRecordError) {
|
||||
throw alteredAuditRecord(error)
|
||||
}
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
async _generateFingerprint(props) {
|
||||
const { oldest = NULL_ID, newest = await this._storage.getLastId() } = props
|
||||
try {
|
||||
return {
|
||||
fingerprint: `${oldest}|${newest}`,
|
||||
nValid: await this._checkIntegrity({ oldest, newest }),
|
||||
}
|
||||
} catch (error) {
|
||||
if (missingAuditRecord.is(error) || alteredAuditRecord.is(error)) {
|
||||
return {
|
||||
fingerprint: `${error.data.id}|${newest}`,
|
||||
nValid: error.data.nValid,
|
||||
error,
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default opts => new AuditXoPlugin(opts)
|
||||
Reference in New Issue
Block a user