feat(xo-server-audit): backup last hash (#5077)
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- [VM/Network] Ability to set VIF TX checksumming [#5095](https://github.com/vatesfr/xen-orchestra/issues/5095) (PR [#5182](https://github.com/vatesfr/xen-orchestra/pull/5182))
|
||||
- [Proxy] Improve health check error messages [#5161](https://github.com/vatesfr/xen-orchestra/issues/5161) (PR [#5191](https://github.com/vatesfr/xen-orchestra/pull/5191))
|
||||
- [VM/network] Ability to change a VIF's locking mode [#4713](https://github.com/vatesfr/xen-orchestra/issues/4713) (PR [#5188](https://github.com/vatesfr/xen-orchestra/pull/5188))
|
||||
- [[Audit] Regularly save fingerprints on remote server for better tempering detection](https://xen-orchestra.com/blog/xo-audit/) [#4844](https://github.com/vatesfr/xen-orchestra/issues/4844) (PR [#5077](https://github.com/vatesfr/xen-orchestra/pull/5077))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -39,6 +40,7 @@
|
||||
>
|
||||
> In case of conflict, the highest (lowest in previous list) `$version` wins.
|
||||
|
||||
- xo-server-audit minor
|
||||
- xo-server patch
|
||||
- xo-server-sdn-controller patch
|
||||
- xo-server minor
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.0",
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
@@ -49,8 +50,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/audit-core": "^0.1.1",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
"lodash": "^4.17.19",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"readable-stream": "^3.5.0",
|
||||
"xo-common": "^0.5.0"
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { alteredAuditRecord, missingAuditRecord } from 'xo-common/api-errors'
|
||||
import { createGzip } from 'zlib'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
import {
|
||||
@@ -135,6 +136,15 @@ class AuditXoPlugin {
|
||||
this._cleaners = []
|
||||
this._xo = xo
|
||||
|
||||
const { enabled = true, schedule: { cron = '0 6 * * *', timezone } = {} } =
|
||||
staticConfig.lastHashUpload ?? {}
|
||||
|
||||
if (enabled) {
|
||||
this._uploadLastHashJob = createSchedule(cron, timezone).createJob(() =>
|
||||
this._uploadLastHash().catch(log.error)
|
||||
)
|
||||
}
|
||||
|
||||
this._auditCore = undefined
|
||||
this._storage = undefined
|
||||
|
||||
@@ -193,6 +203,12 @@ class AuditXoPlugin {
|
||||
oldest: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
const uploadLastHashJob = this._uploadLastHashJob
|
||||
if (uploadLastHashJob !== undefined) {
|
||||
uploadLastHashJob.start()
|
||||
cleaners.push(() => uploadLastHashJob.stop())
|
||||
}
|
||||
|
||||
cleaners.push(
|
||||
this._xo.addApiMethods({
|
||||
audit: {
|
||||
@@ -293,6 +309,41 @@ class AuditXoPlugin {
|
||||
}))
|
||||
}
|
||||
|
||||
// See www-xo#344
|
||||
async _uploadLastHash() {
|
||||
const xo = this._xo
|
||||
const chain = await xo.audit.getLastChain()
|
||||
|
||||
let lastHash, integrityCheckSuccess
|
||||
if (chain !== null) {
|
||||
const hashes = chain.hashes
|
||||
lastHash = hashes[hashes.length - 1]
|
||||
|
||||
// check the integrity of all stored hashes
|
||||
integrityCheckSuccess = await Promise.all(
|
||||
hashes.map((oldest, key) =>
|
||||
oldest !== lastHash
|
||||
? this._checkIntegrity({ oldest, newest: hashes[key + 1] })
|
||||
: true
|
||||
)
|
||||
).then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
}
|
||||
|
||||
// generate a valid fingerprint of all stored records in case of a failure integrity check
|
||||
const { oldest, newest, error } = await this._generateFingerprint({
|
||||
oldest: integrityCheckSuccess ? lastHash : undefined,
|
||||
})
|
||||
|
||||
if (chain === null || !integrityCheckSuccess || error !== undefined) {
|
||||
await xo.audit.startNewChain({ oldest, newest })
|
||||
} else {
|
||||
await xo.audit.extendLastChain({ oldest, newest })
|
||||
}
|
||||
}
|
||||
|
||||
async _checkIntegrity(props) {
|
||||
const { oldest = NULL_ID, newest = await this._storage.getLastId() } = props
|
||||
return this._auditCore.checkIntegrity(oldest, newest).catch(error => {
|
||||
@@ -311,14 +362,18 @@ class AuditXoPlugin {
|
||||
try {
|
||||
return {
|
||||
fingerprint: `${oldest}|${newest}`,
|
||||
newest,
|
||||
nValid: await this._checkIntegrity({ oldest, newest }),
|
||||
oldest,
|
||||
}
|
||||
} catch (error) {
|
||||
if (missingAuditRecord.is(error) || alteredAuditRecord.is(error)) {
|
||||
return {
|
||||
fingerprint: `${error.data.id}|${newest}`,
|
||||
nValid: error.data.nValid,
|
||||
error,
|
||||
fingerprint: `${error.data.id}|${newest}`,
|
||||
newest,
|
||||
nValid: error.data.nValid,
|
||||
oldest: error.data.id,
|
||||
}
|
||||
}
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user