Skip to content

Commit

Permalink
Multiple changes.
Browse files Browse the repository at this point in the history
 * Bundled fleetspeak: make fleetspeak the default.
 * Sandboxing: move sandboxing options from `Platform:` context to `Target:` context.
 * Implement UI for recollecting VFS files.
 * Implement Store layer for recollecting VFS files.
  • Loading branch information
mol123 committed Aug 16, 2021
1 parent ca301d5 commit e178e55
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 19 deletions.
27 changes: 12 additions & 15 deletions grr/core/install_data/etc/grr-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ Target:Darwin:
Client.install_path: |
/usr/local/lib/%(Client.name)/%(ClientRepacker.output_basename)
Client.unprivileged_user: nobody
Client.unprivileged_group: nobody
Client.use_filesystem_sandboxing: True

ClientBuilder.build_dest: "%(Client.name)-build"

ClientBuilder.build_root_dir: /Users/%(USER|env)/mac-build
Expand Down Expand Up @@ -121,6 +125,11 @@ Target:Linux:
PyInstaller.distpath: |
%(PyInstaller.dpkg_root)/debian/
Client.unprivileged_user: _%(Client.name)
Client.unprivileged_group: _%(Client.name)
Client.use_filesystem_sandboxing: True
Client.use_memory_sandboxing: True

Target:LinuxRpm:
ClientBuilder.output_extension: .rpm

Expand Down Expand Up @@ -168,6 +177,9 @@ Target:Windows:

ClientBuilder.template_extension: .exe.zip

Client.use_filesystem_sandboxing: True
Client.use_memory_sandboxing: True

Target:WindowsMsi:
ClientBuilder.output_extension: .msi

Expand Down Expand Up @@ -196,11 +208,6 @@ Platform:Darwin:

Client.platform: darwin

Global Install Context:
Client.unprivileged_user: nobody
Client.unprivileged_group: nobody
Client.use_filesystem_sandboxing: True

Client Context:
Logging.engines: stderr,file,syslog

Expand All @@ -211,12 +218,6 @@ Platform:Linux:

Client.platform: linux

Global Install Context:
Client.unprivileged_user: _%(Client.name)
Client.unprivileged_group: _%(Client.name)
Client.use_filesystem_sandboxing: True
Client.use_memory_sandboxing: True

Client Context:

Logging.engines: stderr,file,syslog
Expand Down Expand Up @@ -244,10 +245,6 @@ Platform:Windows:
- "%(install_path)"
Client.grr_tempdir: "Temp"

Global Install Context:
Client.use_filesystem_sandboxing: True
Client.use_memory_sandboxing: True

Arch:amd64:
ClientBuilder.vs_arch: x64

Expand Down
6 changes: 3 additions & 3 deletions grr/server/grr_response_server/bin/config_updater_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,11 @@ def Prompt(self, config):

if self._IsFleetspeakPresent():
self.use_fleetspeak = RetryBoolQuestion(
"Use Fleetspeak (EXPERIMENTAL, next generation communication "
"framework)?", False)
"Use Fleetspeak (next generation communication "
"framework)?", True)
else:
self.use_fleetspeak = False
print("Fleetspeak (EXPERIMENTAL, optional, next generation "
print("Fleetspeak (next generation "
"communication framework) seems to be missing.")
print("Skipping Fleetspeak configuration.\n")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ <h3 class="mat-h3">File details</h3>
</tr>
</table>

<button mat-stroked-button (click)="recollect()" [disabled]="isRecollecting$ | async" class="recollect">
<mat-spinner *ngIf="isRecollecting$ | async; else enabledIcon" diameter="24"></mat-spinner>
<ng-template #enabledIcon><mat-icon>refresh</mat-icon></ng-template>
Recollect from client
</button>

<pre
class="monospace"
><span *ngFor="let line of textContent$ | async; last as isLast">{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ pre span:before {
width: 100%;
}

.recollect {
align-self: start;
margin-left: 1em;

mat-spinner {
display: inline-block;
}
}

.more-indicator {
display: inline;
background: rgba(0, 0, 0, 0.04);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export class FileDetails implements OnDestroy {
}),
);

readonly isRecollecting$ = this.fileDetailsLocalStore.isRecollecting$;

constructor(
private readonly selectedClientGlobalStore: SelectedClientGlobalStore,
private readonly fileDetailsLocalStore: FileDetailsLocalStore,
Expand All @@ -65,4 +67,8 @@ export class FileDetails implements OnDestroy {
loadMore() {
this.fileDetailsLocalStore.fetchMoreContent(this.DEFAULT_PAGE_LENGTH);
}

recollect() {
this.fileDetailsLocalStore.recollectFile();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatTooltipModule} from '@angular/material/tooltip';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {RouterModule} from '@angular/router';
Expand All @@ -19,6 +20,7 @@ import {FileDetailsRoutingModule} from './routing';
MatButtonModule,
MatIconModule,
MatTooltipModule,
MatProgressSpinnerModule,

FileDetailsRoutingModule,
HumanReadableSizeModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,25 @@ describe('FileDetails Component', () => {
expect(fileDetailsLocalStore.fetchMoreContent)
.toHaveBeenCalledTimes(previousCalls + 1);
});

it('reloads content on "recollect" click', () => {
const fixture = TestBed.createComponent(FileDetails);
fixture.detectChanges();

fileDetailsLocalStore.mockedObservables.textContent$.next('hello');
fixture.detectChanges();

expect(fileDetailsLocalStore.recollectFile).not.toHaveBeenCalled();

const recollectButton = fixture.debugElement.query(By.css('.recollect'));
recollectButton.triggerEventHandler('click', new MouseEvent('click'));
fixture.detectChanges();

expect(fileDetailsLocalStore.recollectFile).toHaveBeenCalledOnceWith();

fileDetailsLocalStore.mockedObservables.textContent$.next('hellonew');
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.textContent)
.toContain('hellonew');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** Test helpers. */
// tslint:disable:no-any

import {TestBed} from '@angular/core/testing';
import {Observable, Subject} from 'rxjs';

import {HttpApiService} from './http_api_service';

type Func = (...args: any[]) => any;

type MockService<T> = {
[K in keyof T]: T[K] extends Function ? jasmine.Spy&T[K] : T[K];
}&{
readonly mockedObservables: {
[K in keyof T]: T[K] extends Func ?
ReturnType<T[K]> extends Observable<infer V>? Subject<V>: never :
never;
}
};

/** HttpApiService with Spy properties and mocked Observable return values. */
export declare interface HttpApiServiceMock extends
MockService<HttpApiService> {}

/**
* Mocks a HttpApiService, replacing methods with jasmine spies that return
* Observables from `httpApiServiceMock.mockedObservables`.
*/
export function mockHttpApiService(): HttpApiServiceMock {
const mockHttpClient = {
get: jasmine.createSpy('get').and.callFake(() => new Subject()),
post: jasmine.createSpy('post').and.callFake(() => new Subject()),
};
const service: any = new HttpApiService(mockHttpClient as any);
const mockedObservables: any = {};

const properties = Object.getOwnPropertyNames(HttpApiService.prototype)
.filter(key => service[key] instanceof Function);

for (const property of properties) {
mockedObservables[property] = new Subject();
service[property] = jasmine.createSpy(property).and.callFake(
() => mockedObservables[property].asObservable());
}

service.mockedObservables = mockedObservables;
return service;
}

/** Injects the MockStore for the given Store class. */
export function injectHttpApiServiceMock(): HttpApiServiceMock {
const mock = TestBed.inject(HttpApiService) as unknown as HttpApiServiceMock;

if (!mock.mockedObservables) {
const val = JSON.stringify(mock).slice(0, 100);
throw new Error(`TestBed.inject(HttpApiService) returned ${
val}, which does not look like HttpApiServiceMock. Did you register the HttpApiService providers?`);
}

return mock;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {ComponentStore} from '@ngrx/component-store';
import {HttpApiService} from '@app/lib/api/http_api_service';
import {Observable} from 'rxjs';
import {Observable, of} from 'rxjs';
import {map, switchMap, tap, withLatestFrom} from 'rxjs/operators';

import {ApiGetFileTextArgsEncoding, ApiGetFileTextResult, PathSpecPathType} from '../lib/api/api_interfaces';
Expand All @@ -11,9 +11,11 @@ import {assertKeyNonNull} from '../lib/preconditions';

interface State {
readonly file?: FileIdentifier;
readonly fetchContentLength?: bigint;
readonly textContent?: string;
readonly totalSize?: bigint;
readonly details?: File;
readonly isRecollecting?: boolean;
}

interface FileIdentifier {
Expand Down Expand Up @@ -47,9 +49,15 @@ export class FileDetailsLocalStore {

readonly hasMore$ = this.store.hasMore$;

readonly isRecollecting$ = this.store.isRecollecting$;

fetchMoreContent(length: bigint) {
this.store.fetchMoreContent(length);
}

recollectFile() {
this.store.recollectFile();
}
}

class FileDetailsComponentStore extends ComponentStore<State> {
Expand Down Expand Up @@ -87,9 +95,13 @@ class FileDetailsComponentStore extends ComponentStore<State> {

readonly fetchMoreContent = this.effect<bigint>(
obs$ => obs$.pipe(
tap(fetchLength => {
this.increaseFetchContentLength(fetchLength);
}),
withLatestFrom(this.state$),
switchMap(([fetchLength, state]) => {
assertKeyNonNull(state, 'file');

return this.httpApiService.getFileText(
state.file.clientId, state.file.pathType, state.file.path, {
encoding: ENCODING,
Expand All @@ -102,13 +114,77 @@ class FileDetailsComponentStore extends ComponentStore<State> {
}),
));

readonly isRecollecting$ =
this.select(state => state.isRecollecting)
.pipe(
map((isRecollecting) => isRecollecting ?? false),
);

readonly setIsRecollecting =
this.updater<boolean>((state, isRecollecting) => ({
...state,
isRecollecting,
}));

readonly recollectFile = this.effect<void>(
obs$ => obs$.pipe(
tap(() => {
this.setIsRecollecting(true);
}),
withLatestFrom(this.state$),
switchMap(([param, state]) => {
assertKeyNonNull(state, 'file');
return this.httpApiService
.updateVfsFileContent(
state.file.clientId, state.file.pathType, state.file.path)
.pipe(
map(details => ({details, state})),
);
}),
switchMap(({details, state}) => {
const content$: Observable<ApiGetFileTextResult|null> =
state.fetchContentLength ?
this.httpApiService.getFileText(
state.file.clientId, state.file.pathType, state.file.path, {
encoding: ENCODING,
offset: 0,
length: state.fetchContentLength,
}) :
of(null);
return content$.pipe(
map(content => ({details, content})),
);
}),
tap(({details, content}) => {
this.setIsRecollecting(false);
this.updateDetails(translateFile(details));
if (content !== null) {
this.setTextContent(content);
}
}),
));

private readonly appendTextContent = this.updater<ApiGetFileTextResult>(
(state, result) => ({
...state,
textContent: (state.textContent ?? '') + (result.content ?? ''),
totalSize: BigInt(result.totalSize ?? 0),
}));


private readonly setTextContent = this.updater<ApiGetFileTextResult>(
(state, result) => ({
...state,
textContent: result.content ?? '',
totalSize: BigInt(result.totalSize ?? 0),
}));

private readonly increaseFetchContentLength = this.updater<bigint>(
(state, length) => ({
...state,
fetchContentLength: (state.fetchContentLength ?? BigInt(0)) + length,
}));

private readonly updateDetails =
this.updater<File>((state, details) => ({...state, details}));
}
Loading

0 comments on commit e178e55

Please sign in to comment.