Add tags to standard query library and fleetdm.com/queries (#3970)
* handle query tags in build-static-content script, update query readme * show tags in query library, add ability to filter by tags * fix lint errors * update mobile styles * fix CTA link * update mobile layout * remove tag line-height and font size * Update build-static-content.js * Style update * remove margin from selected tag, adjust OS logo placement * requested changes from code review Co-authored-by: Mike Thomas <mthomas@fleetdm.com>
This commit is contained in:
@@ -27,6 +27,7 @@ Want to add your own query?
|
||||
purpose: What is the goal of running your query? Ex. Detection
|
||||
remediation: Are there any remediation steps to resolve the detection triggered by your query? If not, insert "N/A."
|
||||
contributors: zwass,mike-j-thomas
|
||||
tags: Keywords that can help users find other relevant queries, each tag should be seperated by a comma. (e.g., "foo,bar")
|
||||
```
|
||||
|
||||
2. Replace each field and submit a pull request to the fleetdm/fleet GitHub repository.
|
||||
|
||||
+12
-1
@@ -8,6 +8,7 @@ parasails.registerPage('query-library', {
|
||||
searchString: '', // The user input string to be searched against the query library
|
||||
selectedKind: 'all queries', // Initially set to all, the user may select a different option to filter queries by purpose (e.g., "all queries", "informational", "policies")
|
||||
selectedPlatform: 'all platforms', // Initially set to all, the user may select a different option to filter queries by platform (e.g., "all platforms", "macOS", "Windows", "Linux")
|
||||
selectedTag: '', // Initially set to a blank string, the user may select a tag filter queries by.
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -15,7 +16,8 @@ parasails.registerPage('query-library', {
|
||||
return this.queries.filter(
|
||||
(query) =>
|
||||
this._isIncluded(query.platforms, this.selectedPlatform) &&
|
||||
this._isIncluded(query.kind, this.selectedKind)
|
||||
this._isIncluded(query.kind, this.selectedKind) &&
|
||||
this._isIncluded(query.tags, this.selectedTag)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -50,6 +52,10 @@ parasails.registerPage('query-library', {
|
||||
this.selectedPlatform = platform;
|
||||
},
|
||||
|
||||
clickSelectTag(tag) {
|
||||
this.selectedTag = tag;
|
||||
},
|
||||
|
||||
clickCard: function (querySlug) {
|
||||
window.location = '/queries/' + querySlug; // we can trust the query slug is url-safe
|
||||
},
|
||||
@@ -111,6 +117,11 @@ parasails.registerPage('query-library', {
|
||||
textToSearch += ', ' + normalize(contributor.name) + ', ' + normalize(contributor.handle);
|
||||
});
|
||||
}
|
||||
if (query.tags) {
|
||||
query.tags.forEach((tag) => {
|
||||
textToSearch += ', ' + normalize(tag);
|
||||
});
|
||||
}
|
||||
return (searchTerms.some((term) => textToSearch.includes(term)));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
border-color: #E2E4EA;
|
||||
}
|
||||
|
||||
[purpose='query-tag'] {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
background-color: #E2E4EA;
|
||||
}
|
||||
|
||||
[purpose='remediation'] {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
|
||||
+35
-1
@@ -36,6 +36,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
[purpose='query-tag'] {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
background-color: #E2E4EA;
|
||||
}
|
||||
|
||||
[purpose='selected-tag'] {
|
||||
border-bottom: 1px solid #e1e4e9;
|
||||
padding: 16px 0;
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
&.search {
|
||||
width: 250px;
|
||||
@@ -205,7 +224,7 @@
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
.query-card {
|
||||
background-color: #f1f0ff;
|
||||
background-color: #F8F7FF;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
@@ -221,6 +240,13 @@
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 90px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
p {
|
||||
margin-block-end: 0px;
|
||||
}
|
||||
a {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -268,6 +294,14 @@
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
[purpose='selected-tag'] {
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
[purpose='query-tag'] {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
+23
@@ -30,6 +30,7 @@ module.exports = {
|
||||
|
||||
let queriesWithProblematicResolutions = [];
|
||||
let queriesWithProblematicContributors = [];
|
||||
let queriesWithProblematicTags = [];
|
||||
let queries = YAML.parseAllDocuments(yaml).map((yamlDocument)=>{
|
||||
let query = yamlDocument.toJSON().spec;
|
||||
query.kind = yamlDocument.toJSON().kind;
|
||||
@@ -40,6 +41,25 @@ module.exports = {
|
||||
} else if (query.resolution === undefined) {
|
||||
query.resolution = 'N/A';// « We set this to a string here so that the data type is always string. We use N/A so folks can see there's no remediation and contribute if desired.
|
||||
}
|
||||
if (query.tags) {
|
||||
if(!_.isString(query.tags)) {
|
||||
queriesWithProblematicTags.push(query);
|
||||
} else {
|
||||
// Splitting tags into an array to format them.
|
||||
let tagsToFormat = query.tags.split(',');
|
||||
let formattedTags = [];
|
||||
for (let tag of tagsToFormat) {
|
||||
if(tag !== '') {// « Ignoring any blank tags caused by trailing commas in the YAML.
|
||||
// Formatting tags in sentence case, and removing any extra whitespace.
|
||||
formattedTags.push(_.capitalize(_.trim(tag)));
|
||||
}
|
||||
}
|
||||
// Removing any duplicate tags.
|
||||
query.tags = _.uniq(formattedTags);
|
||||
}
|
||||
} else {
|
||||
query.tags = []; // « if there are no tags, we set query.tags to an empty array so it is always the same data type.
|
||||
}
|
||||
|
||||
// GitHub usernames may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen.
|
||||
if (!query.contributors || (query.contributors !== undefined && !_.isString(query.contributors)) || query.contributors.split(',').some((contributor) => contributor.match('^[^A-za-z0-9].*|[^A-Za-z0-9-]|.*[^A-za-z0-9]$'))) {
|
||||
@@ -52,6 +72,9 @@ module.exports = {
|
||||
if (queriesWithProblematicResolutions.length >= 1) {
|
||||
throw new Error('Failed parsing YAML for query library: The "resolution" of a query should either be absent (undefined) or a single string (not a list of strings). And "resolution" should only be present when a query\'s kind is "policy". But one or more queries have an invalid "resolution": ' + _.pluck(queriesWithProblematicResolutions, 'slug').sort());
|
||||
}//•
|
||||
if (queriesWithProblematicTags.length >= 1) {
|
||||
throw new Error('Failed parsing YAML for query library: The "tags" of a query should either be absent (undefined) or a single string (not a list of strings). "tags" should be be be seperated by a comma. But one or more queries have invalid "tags": ' + _.pluck(queriesWithProblematicTags, 'slug').sort());
|
||||
}
|
||||
// Assert uniqueness of slugs.
|
||||
if (queries.length !== _.uniq(_.pluck(queries, 'slug')).length) {
|
||||
throw new Error('Failed parsing YAML for query library: Queries as currently named would result in colliding (duplicate) slugs. To resolve, rename the queries whose names are too similar. Note the duplicates: ' + _.pluck(queries, 'slug').sort());
|
||||
|
||||
Vendored
+11
@@ -41,6 +41,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-top py-2 pb-md-4">
|
||||
<div class="d-flex flex-md-column justify-content-between justify-content-md-start align-items-center align-items-md-start pt-md-3">
|
||||
<h5 class="pb-md-2 m-0">Tags</h5>
|
||||
<div :class="[query.tags.length > 2 ? 'mt-1' : '']" class="d-flex flex-wrap align-items-center justify-content-end justify-content-sm-start">
|
||||
<span class="mb-1" v-if="!query.tags || !query.tags.length">--</span>
|
||||
<div purpose="query-tag" class="mr-md-1 ml-1 ml-md-0" :class="[query.tags.length > 2 ? 'mb-1' : '']" v-for="tag in query.tags">
|
||||
{{tag}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-top py-2">
|
||||
<div class="d-flex flex-md-column justify-content-between justify-content-md-start align-items-center align-items-md-start py-1 py-md-3">
|
||||
<h5 class="pb-md-2 m-0">Purpose</h5>
|
||||
|
||||
+15
-6
@@ -92,18 +92,28 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="results">
|
||||
<div purpose="selected-tag" v-if="selectedTag">
|
||||
<p>Showing {{ selectedKind === 'query' ? 'informational queries' : selectedKind === 'policy' ? 'policies' : 'all queries'}} for <span purpose="query-tag" class="d-inline-block" style="cursor: pointer;" @click="clickSelectTag('')">{{selectedTag}}<span style="color: #8b8fa2;" class="fa fa-times-circle pl-2"></span></span></p>
|
||||
</div>
|
||||
<div class="category__informational">
|
||||
<div v-for="query of queriesList">
|
||||
<div class="card results" @click="clickCard(query.slug)">
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-between align-items-center query-card">
|
||||
<div class="col-12">
|
||||
<div class="d-block d-sm-flex flex-wrap">
|
||||
<h5 class="card-title m-0 mb-1 mr-sm-2">{{query.name}}</h5>
|
||||
<div class="my-2 my-sm-0 flex-wrap">
|
||||
<span class="mr-2 mb-1 text-nowrap d-inline-block" purpose="query-tag" v-for="tag in query.tags" @click.stop="clickSelectTag(tag)">{{tag}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9 col-md-9">
|
||||
<h5 class="card-title m-0">{{query.name}}</h5>
|
||||
<p class="font-italic mb-1 p-0 description">{{query.description}}</p>
|
||||
<div class="contributors" v-if="query.contributors && query.contributors.length">
|
||||
<div class="d-flex mb-2 mb-sm-1 align-items-center">
|
||||
<div v-for="contributor in query.contributors">
|
||||
<div class="d-flex m-1 avatar-frame" @click="clickAvatar(contributor)">
|
||||
<div class="d-flex m-1 avatar-frame" @click.stop="clickAvatar(contributor)">
|
||||
<img alt="a GitHub user avatar" :alt="contributor" :src="contributor.avatarUrl" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +121,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 col-md-auto">
|
||||
<div class="col-sm-3 col-md-auto align-self-start">
|
||||
<div class="text-sm-right m-0">
|
||||
<img class="d-inline-flex mr-1 mr-sm-0 ml-sm-1 ml-md-2 logo"
|
||||
src="/images/os-macos-black-16x16@2x.png" alt="macOS"
|
||||
@@ -138,12 +148,11 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-center p-3">
|
||||
<div class="card call-to-action col-6-grow my-5 library">
|
||||
<div class="card-body">
|
||||
<div class="card-body px-0">
|
||||
<h3 class="mb-3">Contributors</h3>
|
||||
<p><strong>Want to add your own query?</strong> Please submit a pull request
|
||||
<a href="https://github.com/fleetdm/fleet/edit/main/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml">
|
||||
over on GitHub
|
||||
</a>.
|
||||
over on GitHub</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user