CN-1173: 检测功能UI开发

This commit is contained in:
刘洪洪
2023-08-03 18:47:18 +08:00
parent 238f452643
commit f71e27339d
24 changed files with 3671 additions and 103 deletions

View File

@@ -89,3 +89,9 @@
@import "views/entityExplorer/graphRightListBlock";
@import "views/entityExplorer/graphRightDetailBlock";
@import "views/detections/detection-tools";
@import "views/detections/detection-drawer";
@import "views/detections/detection-table";
@import "views/detections/detection-create/detection-form";
@import "views/detections/detection-create/detection-form-setting";

View File

@@ -0,0 +1,515 @@
.form-setting__block {
width: 620px;
.el-form-item__label {
height: 14px;
line-height: 14px;
padding: 0;
margin-bottom: 12px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #333333;
font-weight: 500;
}
.el-form-item__content {
height: 24px;
line-height: 24px;
}
.block-mode {
width: 300px;
height: 125px;
padding: 16px;
background: #FFFFFF;
border: 1px solid rgba(226, 229, 236, 1);
border-radius: 2px;
margin-right: 20px;
display: flex;
.block-mode-left {
line-height: 16px;
margin-right: 10px;
.block-mode-icon {
font-size: 16px;
}
}
.block-mode-right {
width: 240px;
display: flex;
flex-direction: column;
.block-mode-title {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
line-height: 14px;
}
.block-mode-content {
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #717171;
line-height: 15px;
font-weight: 400;
margin: 10px 0;
}
.block-mode-btn, .block-mode-btn-active {
width: 240px;
height: 28px;
line-height: 28px;
text-align: center;
background: #F5F6F7;
border-radius: 2px;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #353636;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.block-mode-btn-active {
background: #7E9F54;
color: #FFFFFF;
}
}
}
.block-title {
font-family: NotoSansHans-Medium;
font-size: 14px;
line-height: 14px;
color: #333333;
font-weight: 500;
margin-bottom: 12px;
}
.block-title1 {
font-family: NotoSansHans-Medium;
font-size: 12px;
line-height: 12px;
color: #353636;
font-weight: 500;
margin-bottom: 12px;
}
.form-setting__select {
width: 620px !important;
.el-input--mini .el-input__inner {
height: 24px !important;
line-height: 24px !important;
}
}
.form-setting__input {
&.el-input {
height: 24px !important;
line-height: 24px !important;
}
.el-input__inner {
height: 24px;
line-height: 24px;
}
}
.form-setting__textarea {
.el-textarea__inner {
height: 110px;
}
}
.el-switch__label, .form-setting__block .el-switch__label {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
}
.block-dimension {
width: 620px;
height: 24px;
line-height: 24px;
border: 1px solid #e2e5ec;
display: flex;
align-items: center;
.block-dimension-tag {
width: auto;
height: 19px;
line-height: 19px;
background: #E5E8EB;
border-radius: 2px;
opacity: 0.6;
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #353636;
font-weight: 400;
margin-right: 4px;
}
}
.el-input--mini .el-input__inner {
height: 24px !important;
line-height: 24px !important;
}
}
.form-setting__block-key {
display: flex;
align-items: center;
margin-bottom: 16px;
.block-key {
height: 28px;
background: #F5F8FA;
border: 1px solid rgba(222, 222, 222, 1);
box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.02);
border-radius: 2px;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #353636;
font-weight: 500;
margin-left: 12px;
display: flex;
align-items: center;
padding: 0 12px;
cursor: pointer;
i {
font-size: 12px;
margin-right: 4px;
color: #575757;
}
}
}
.form-setting__btn, .form-setting__btn1 {
width: 100%;
display: flex;
justify-content: flex-end;
.el-button {
height: 30px !important;
min-height: 30px !important;
line-height: 30px !important;
background: #38ACD2;
border: 1px solid rgba(46, 136, 166, 1);
border-radius: 2px;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #FFFFFF;
font-weight: 500;
padding: 0 14px;
}
}
.form-setting__btn1 {
.el-button {
padding: 0 11px !important;
}
}
.key-drawer {
position: absolute;
top: 0;
right: 0;
.el-input__inner {
border-color: #DEDEDE !important;
}
}
.definition-filter-block {
border: 1px #E2E5EC solid;
min-height: 196px;
display: flex;
flex-direction: column;
padding: 12px;
position: relative;
.definition-filter-item {
display: flex;
align-items: center;
line-height: 24px;
margin-bottom: 6px;
.filter-item__select {
width: 220px;
}
.filter-item__input {
width: 170px;
}
.el-input--mini .el-input__inner {
height: 24px;
line-height: 24px;
}
i {
font-size: 12px !important;
font-variant-caps: all-small-caps;
color: #575757;
cursor: pointer;
}
}
.filter-block-footer {
position: absolute;
width: 100%;
bottom: 0;
height: 24px;
line-height: 24px;
text-align: center;
i {
font-size: 12px;
font-variant-caps: all-small-caps;
color: #575757;
cursor: pointer;
}
}
}
.block-filter-add {
height: 24px;
line-height: 24px;
background: #F5F8FA;
border: 1px solid rgba(222, 222, 222, 1);
box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.02);
border-radius: 2px;
color: #575757;
text-align: center;
cursor: pointer;
}
.history-top-key {
width: 396px;
.el-drawer__body {
border: 1px #E2E5EC solid;
}
.el-overlay {
position: absolute;
}
.el-drawer, .rtl {
width: 100% !important;
}
.key-table {
.el-table th > .cell, .el-table .cell {
padding-left: 0 !important;
padding-right: 0 !important;
line-height: 28px;
}
.el-table th {
font-family: NotoSansHans-Medium;
font-weight: 500;
padding: 0 0 0 12px;
color: #353636;
background: #F9F9F9;
height: 30px;
border: 0 !important;
}
.el-table__body td {
font-family: NotoSansSChineseRegular;
font-weight: 400;
padding: 0 0 0 12px;
font-size: 14px;
color: #353636;
height: 28px;
border: 0 !important;
}
.el-table::before {
background-color: #FFFFFF;
}
}
}
.definition-condition-block {
border: 1px #E2E5EC solid;
padding: 20px;
margin-bottom: 10px;
.condition-title {
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #333333;
font-weight: 500;
margin-bottom: 12px;
height: 14px;
line-height: 14px;
}
.condition__select {
width: 100%;
height: 24px;
line-height: 24px;
.el-input__inner {
padding-left: 20px !important;
}
.condition__select__icon {
width: 4px;
height: 12px;
border-radius: 2px;
margin-left: 6px;
}
}
.condition-metric {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
display: flex;
flex-direction: column;
.condition-metric-item1 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.metric-item1__text {
height: 24px;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
display: flex;
}
i {
font-size: 12px;
font-variant-caps: all-small-caps;
cursor: pointer;
}
.metric-item1-close {
color: #E26154;
margin-left: 12px;
}
.metric-item1-close-disable {
color: #ecb2ad;
margin-left: 12px;
cursor: no-drop;
}
}
.condition-metric-item2 {
height: 24px;
line-height: 24px;
display: flex;
align-items: center;
justify-content: flex-start;
.metric-item2__select {
margin-right: 12px;
.el-input__inner {
width: 170px;
}
}
.metric-item2__input {
width: 112px;
margin-right: 12px;
height: 24px !important;
margin-top: -1px !important;
border-radius: 2px !important;
.el-input__inner {
width: 112px;
}
}
}
.el-divider--horizontal {
margin: 12px 0;
}
}
}
.condition__select {
width: 100%;
height: 24px;
line-height: 24px;
.el-input__inner {
padding-left: 20px !important;
}
.condition__select__icon {
width: 4px;
height: 12px;
border-radius: 2px;
margin-left: 6px;
}
}
.condition-add {
width: 100%;
height: 24px;
background: #F5F8FA;
border: 1px solid rgba(222, 222, 222, 1);
box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.02);
border-radius: 2px;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #353636;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 14px;
font-variant-caps: all-small-caps;
margin-right: 4px;
margin-top: -2px;
}
}
.condition-divider {
border: 1px #ECECEC dashed;
.el-divider__text {
padding: 0 4px;
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #BEBEBE;
font-weight: 400;
}
}
.margin-r-8 {
margin-right: 8px;
}
.margin-b-20 {
margin-bottom: 20px;
}
.margin-b-10 {
margin-bottom: 10px;
}
.margin-t-18 {
margin-top: 18px;
}

View File

@@ -0,0 +1,140 @@
.detection-form {
padding: 20px;
.detection-form-header {
font-family: NotoSansHans-Black;
font-size: 24px;
color: #353636;
font-weight: 900;
margin-top: 10px;
}
.detection-form-content {
height: 100%;
overflow: scroll;
padding-bottom: 40px;
.detection-form-collapse {
margin-top: 20px;
width: 1200px;
.el-collapse-item__header {
width: 1200px !important;
height: 56px !important;
background: #FFFFFF !important;
border-left: 1px solid #EFF2F5;
border-right: 1px solid #EFF2F5;
//box-shadow: 0 2px 4px 0 rgba(51,51,51,0.02) !important;
border-radius: 4px !important;
display: flex;
justify-content: space-between;
flex-direction: inherit;
position: relative;
padding: 0 20px;
i {
position: absolute;
left: 20px;
}
}
.el-collapse-item__wrap {
border: 1px solid #EFF2F5;
border-top: none;
}
.form-collapse-header {
margin-left: 26px;
display: flex;
align-items: center;
.form-collapse-header-no, .form-collapse-header-no-active {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
font-family: NotoSansHans-Medium;
font-size: 16px;
font-weight: 500;
border-radius: 50%;
margin-right: 10px;
transition: 0.5s all;
}
.form-collapse-header-no {
background: #E2E5EC;
color: #333333;
}
.form-collapse-header-no-active {
background: #38ACD2;
color: #FFFFFF;
}
.form-collapse-header-title {
font-family: NotoSansHans-Medium;
font-size: 16px;
color: #333333;
font-weight: 500;
}
}
.form-collapse-content {
padding: 0 20px 0 46px;
.trigger-block {
width: 620px;
height: 90px;
padding: 16px 12px;
border-radius: 2px;
border: 1px #E2E5EC solid;
display: flex;
flex-direction: column;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
.trigger-block-item {
display: flex;
align-items: flex-start;
justify-content: flex-start;
height: 24px;
line-height: 24px;
}
.el-input--mini .el-input__inner {
width: 112px;
margin: 0 12px;
}
.el-select .el-input .el-select__caret {
height: 24px;
line-height: 24px;
margin-right: 12px;
}
.form-trigger__select {
margin-left: -12px;
}
.el-form-item__content {
height: 24px;
line-height: 24px !important;
}
}
}
}
.el-input--mini, .el-input--mini .el-input__inner {
height: 24px !important;
line-height: 24px !important;
border-radius: 2px !important;
}
}
.el-collapse-item__content {
padding-bottom: 20px;
}
}

View File

@@ -0,0 +1,94 @@
.detection-drawer {
padding: 20px;
height: 100%;
overflow: scroll;
.el-drawer {
width: 440px !important;
}
.el-collapse-item__content {
padding-bottom: 0 !important;
}
.el-overlay {
top: 100px !important;
background-color: rgba(0, 0, 0, 0.16) !important;
}
.detection-drawer-title, .basic-function-value, basic-description-value {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #717171;
font-weight: 400;
line-height: 14px;
display: flex;
}
.drawer-basic {
.drawer-basic-header {
font-family: NotoSansHans-Black;
font-size: 16px;
color: #353636;
font-weight: 900;
margin-bottom: 20px;
.drawer-basic-id {
margin-bottom: 10px;
}
}
}
.drawer-basic-function, .drawer-basic-description {
margin-bottom: 20px;
}
.detection-drawer-title {
margin-bottom: 8px;
}
.basic-description-value {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
line-height: 18px;
font-weight: 400;
}
.basic-function-value {
color: #046ECA;
}
.detection-drawer-collapse {
background: #FFFFFF;
border: 1px solid rgba(226, 229, 236, 1);
border-radius: 4px;
//box-shadow: 0 1px 0 0 rgba(226, 229, 236, 1);
.el-collapse-item__header {
height: 32px !important;
background-color: #F7F7F7 !important;
padding-left: 12px !important;
border-radius: 4px 4px 0 0;
}
.drawer-collapse-content {
padding: 20px 20px 0;
.detection__icon {
width: 4px;
height: 12px;
border-radius: 2px;
margin-right: 6px;
}
.drawer-collapse-trigger {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
line-height: 18px;
font-weight: 400;
margin-bottom: 20px;
}
}
}
}

View File

@@ -1,96 +1,165 @@
.detection-filter-case {
display: flex;
flex-direction: column;
width: 280px;
padding: 10px;
margin-right: 10px;
background-color: white;
overflow: auto;
//.detection-filter-case {
// display: flex;
// flex-direction: column;
// width: 280px;
// padding: 10px;
// margin-right: 10px;
// background-color: white;
// overflow: auto;
//
// .detection-filter {
// display: flex;
// flex-direction: column;
// margin-bottom: 10px;
//
// .filter__header {
// display: flex;
// flex: 0 0 32px;
// align-items: center;
// padding-left: 10px;
// color: #666;
// //background-color: #F3F7FA;
// cursor: pointer;
//
// span {
// font-size: 14px;
// padding-left: 6px;
// }
// i {
// font-size: 12px;
// transition: all linear .1s;
// transform: rotate(0) translate(0, 2px);
// }
// i.arrow-rotate {
// transform: rotate(90deg) translate(2px, 3px);
// }
// .new-detection-filter-header-title {
// font-size: 14px;
// color: #353636;
// font-weight: 600;
// }
// .new-detection-filter-icon {
// margin-left: 8px;
// margin-bottom: 2px;
// font-weight: bold !important;
// }
// }
//
// .filter__body {
// padding: 5px 0 0 15px;
//
// .el-checkbox-group {
// display: flex;
// flex-direction: column;
//
// .el-checkbox {
// display: flex;
// align-items: center;
// padding: 5px 0;
// margin-right: 5px;
// .el-checkbox__label {
// width: 100%;
// }
//
// .filter__checkbox-label {
// display: flex;
// justify-content: space-between;
// align-items: center;
//
// .severity-color-block {
// width: 4px;
// height: 15px;
// border-radius: 2px;
// }
// }
//
// &:last-of-type {
// padding-bottom: 0;
// }
// }
// }
// }
// }
// .new-detection-filter-title {
// display: flex;
// flex: 0 0 32px;
// align-items: center;
// padding-left: 27px;
// background-color: #EFF2F5;
// cursor: pointer;
// font-size: 14px;
// color: #353636;
// font-weight: 600;
// margin: -10px;
// margin-bottom: 10px;
// }
//}
.detection-filter-title {
height: 32px;
line-height: 32px;
background: #F7F7F7;
padding: 0 20px;
box-shadow: 0 1px 0 0 rgba(226, 229, 236, 1);
border-radius: 4px 4px 0 0;
}
.detection-filter-content {
padding: 20px;
.detection-filter {
display: flex;
flex-direction: column;
margin-bottom: 10px;
.filter__header {
display: flex;
flex: 0 0 32px;
align-items: center;
padding-left: 10px;
color: #666;
//background-color: #F3F7FA;
cursor: pointer;
span {
.filter-content-title {
font-family: NotoSansHans-Medium;
font-size: 14px;
padding-left: 6px;
line-height: 14px;
margin-bottom: 10px;
color: #353636;
font-weight: 500;
}
i {
font-size: 12px;
transition: all linear .1s;
transform: rotate(0) translate(0, 2px);
}
i.arrow-rotate {
transform: rotate(90deg) translate(2px, 3px);
}
.new-detection-filter-header-title {
.filter-content-content {
display: flex;
flex-direction: column;
.filter-content-checkbox {
line-height: 16px;
margin-bottom: 10px;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 600;
}
.new-detection-filter-icon {
margin-left: 8px;
margin-bottom: 2px;
font-weight: bold !important;
}
font-weight: 400;
.el-checkbox__inner {
width: 16px !important;
height: 16px !important;
text-align: center !important;
line-height: 16px !important;
}
.filter__body {
padding: 5px 0 0 15px;
.el-checkbox-group {
display: flex;
flex-direction: column;
.el-checkbox {
display: flex;
align-items: center;
padding: 5px 0;
margin-right: 5px;
.el-checkbox__label {
width: 100%;
.el-checkbox__input.is-indeterminate .el-checkbox__inner {
border-color: #38ACD2;
background: #38ACD2;
border-radius: 2px;
}
.filter__checkbox-label {
display: flex;
justify-content: space-between;
align-items: center;
.severity-color-block {
width: 4px;
height: 15px;
.el-checkbox__input.is-indeterminate .el-checkbox__inner:before {
background: #FFFFFF;
border-radius: 1px;
}
.el-checkbox__input.is-checked {
.el-checkbox__inner {
border-color: #38ACD2;
background: #38ACD2;
border-radius: 2px;
}
}
.el-checkbox__input.is-focus {
.el-checkbox__inner {
border-color: #38ACD2;
}
}
&:last-of-type {
padding-bottom: 0;
}
}
}
}
}
.new-detection-filter-title {
display: flex;
flex: 0 0 32px;
align-items: center;
padding-left: 27px;
background-color: #EFF2F5;
cursor: pointer;
.el-checkbox__label {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 600;
margin: -10px;
margin-bottom: 10px;
font-weight: 400;
}
}
}
}

View File

@@ -0,0 +1,83 @@
.detection-table {
.el-table th > .cell, .el-table .cell {
padding-left: 0 !important;
padding-right: 0 !important;
line-height: 16px;
}
.el-table--enable-row-transition .el-table__body td, .el-table--border th {
border-left: 0 !important;
border-right: 0 !important;
font-size: 12px;
color: #353636;
}
.el-table--border th {
font-family: NotoSansHans-Medium;
font-weight: 500;
padding: 0;
}
.el-table__header-wrapper {
height: 32px !important;
line-height: 32px !important;
border-bottom: 1px solid #EBEEF5;
}
.el-table__body td {
font-family: NotoSansSChineseRegular;
font-weight: 400;
}
.el-table__row {
height: 32px !important;
}
}
.detection-tag-blue, .detection-tag-red, .detection-tag-gray, .detection-tag-status0, .detection-tag-status1 {
display: inline-block;
border-radius: 10px;
font-family: NotoSansSChineseRegular;
font-size: 12px;
font-weight: 400;
padding: 1px 12px;
line-height: 20px;
}
.detection-tag-blue {
background: rgba(56, 172, 210, 0.10);
box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.02);
color: #046ECA;
}
.detection-tag-red {
background: rgba(226, 97, 84, 0.12);
color: #E26154;
}
.detection-tag-gray {
background: rgba(113, 113, 113, 0.12);
color: #717171;
}
.detection-tag-status0 {
font-weight: 500;
font-family: NotoSansHans-Medium;
background: rgba(113, 113, 113, 0.12);
color: #717171;
padding: 0 12px;
}
.detection-tag-status1 {
font-weight: 500;
font-family: NotoSansHans-Medium;
background: rgba(126, 159, 84, 0.12);
color: #7E9F54;
padding: 0 8px;
}
.detection-table-library {
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #046ECA;
font-weight: 400;
}

View File

@@ -0,0 +1,154 @@
.top-tools__left {
display: flex;
align-items: center;
flex-direction: row;
.top-tool-btn {
cursor: pointer;
height: 28px;
width: 28px;
border: 1px solid #DEDEDE;
outline: none;
border-radius: 2px;
background-color: #F9F9F9;
transition: background-color linear .1s;
font-size: 12px;
font-weight: 500;
//font-family: $fontFamily !important;
i {
font-size: 14px;
color: #575757;
margin-right: 4px;
}
}
.top-tool-btn:disabled {
cursor: not-allowed;
opacity: 0.66;
i {
}
}
.top-tool-btn:hover:not(.cn-btn-disabled) {
border: 1px solid #DEDEDE;
background-color: #EBF1F4;
}
.top-tool-btn:focus:not(.cn-btn-disabled), .top-tool-btn.is-focus {
background-color: #E0E7EA;
border: 1px solid #DEDEDE;
i {
color: #575757;
}
}
.top-tool-btn--delete.top-tool-btn:focus:not(.cn-btn-disabled) {
background-color: #EBF1F4;
border-color: #FFC4B9;
i {
color: #F0745A;
}
}
.top-tool-btn--create {
background-color: #38ACD2 !important;
border-color: #2E88A6 !important;
color: #FFFFFF;
i {
color: #FFFFFF;
}
}
.top-tool-btn--create:hover {
background-color: #57B8D9 !important;
border-color: #2E88A6 !important;
color: #FFFFFF;
i {
color: #FFFFFF;
}
}
.top-tool-btn--create:focus {
background-color: #31A5CD !important;
border-color: #2E88A6 !important;
color: #FFFFFF !important;
i {
color: #FFFFFF !important;
}
}
.top-tool-btn--create:disabled {
opacity: 0.66;
background-color: #38ACD2 !important;
border-color: #2E88A6 !important;
color: #FFFFFF;
i {
color: #FFFFFF;
}
}
.top-tool-search {
display: flex;
width: 242px; //calc(100% - 256px);
.el-input--small {
line-height: 27px;
.el-input__inner {
height: 28px;
border-radius: 2px 0 0 2px;
}
}
.top-tool-btn {
border-left: none;
border-radius: 0 2px 2px 0 !important;
}
.top-tool-btn--search:hover {
border-left: none !important;
border-radius: 0 2px 2px 0 !important;
}
.top-tool-btn--search:focus {
border-left: none !important;
border-radius: 0 2px 2px 0 !important;
}
}
.top-tool-search {
display: flex;
width: 100%;
.el-input--small {
line-height: 27px;
.el-input__inner {
height: 28px;
border-radius: 2px 0 0 2px;
}
}
.top-tool-btn {
border-left: none;
border-radius: 0 2px 2px 0 !important;
}
.top-tool-btn--search:hover {
border-left: none !important;
border-radius: 0 2px 2px 0 !important;
}
.top-tool-btn--search:focus {
border-left: none !important;
border-radius: 0 2px 2px 0 !important;
}
}
}

View File

@@ -0,0 +1,194 @@
<template>
<div>
<div class="form-setting__block margin-b-20" style="display: flex">
<div class="block-mode">
<!--todo 图标没有后期换-->
<div class="block-mode-left">
<i class="cn-icon cn-icon-setting2 block-mode-icon"></i>
</div>
<div class="block-mode-right">
<div class="block-mode-title">Indicator Match</div>
<div class="block-mode-content">
Use indicators from intelligencesources to detect matchingevents and alerts.
</div>
<div :class="settingObj.ruleType===detectionRuleType.indicator?'block-mode-btn-active':'block-mode-btn'"
@click="selectMode(detectionRuleType.indicator)">select
</div>
</div>
</div>
<div class="block-mode">
<!--todo 图标没有后期换-->
<div class="block-mode-left">
<i class="cn-icon cn-icon-setting2 block-mode-icon"></i>
</div>
<div class="block-mode-right">
<div class="block-mode-title">Threshold</div>
<div class="block-mode-content">
Aggregate query results to detect when number of matches exceeds threshold.
</div>
<div :class="settingObj.ruleType===detectionRuleType.threshold?'block-mode-btn-active':'block-mode-btn'"
@click="selectMode(detectionRuleType.threshold)">select
</div>
</div>
</div>
</div>
<!--category-->
<el-form ref="form" :model="settingObj" label-position="top" :rules="rules">
<el-form-item label="Category" prop="category" class="form-setting__block margin-b-20">
<el-select v-model="settingObj.category" class="form-setting__select" placeholder=" " size="mini" @change="changeEditFlag">
<el-option
v-for="item in categoryList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!--type-->
<el-form-item label="Type" prop="eventType" class="form-setting__block margin-b-20">
<el-select v-model="settingObj.eventType" placeholder=" " size="mini" class="form-setting__select" @change="changeEditFlag">
<el-option
v-for="item in typeList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!--name-->
<el-form-item label="Name" prop="name" class="form-setting__block margin-b-20">
<el-input
maxlength="64"
show-word-limit
placeholder=""
v-model="settingObj.name"
@input="changeEditFlag"
class="form-setting__input" />
</el-form-item>
<!--Description-->
<div class="form-setting__block margin-b-20">
<div class="block-title">Description</div>
<el-input
maxlength="255"
show-word-limit
v-model="settingObj.description"
type="textarea"
resize='none'
@input="changeEditFlag"
class="form-setting__textarea" />
</div>
</el-form>
<div class="form-setting__block margin-b-20">
<div class="block-title">Policy Status</div>
<el-switch
v-model="settingObj.status"
@change="changeEditFlag"
active-color="#38ACD2"
inactive-color="#C0CEDB"
:active-value="1"
:inactive-value="0"
:active-text="switchStatus(settingObj.status)"/>
</div>
<div class="form-setting__btn">
<el-button @click="onContinue">Continue</el-button>
</div>
</div>
</template>
<script>
import { detectionRuleType } from '@/utils/constants'
import { switchStatus } from '@/utils/tools'
import { get } from '@/utils/http'
import { api } from '@/utils/api'
export default {
name: 'GeneralSettings',
data () {
return {
detectionRuleType,
categoryList: [],
typeList: [],
settingObj: {
ruleType: detectionRuleType.threshold,
category: '',
eventType: '',
name: '',
description: '',
status: 1,
editFlag: false, // 编辑标识如果保存之后继续编辑且不再点击保存置为true则不会将编辑内容提交到最终form
saveFlag: false // 是否点击保存标识
},
rules: {
category: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
eventType: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
name: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'blur'
}
]
}
}
},
mounted () {
this.initData()
},
methods: {
switchStatus,
initData () {
get(api.detection.statistics, { pageSize: -1 }).then(response => {
if (response.code === 200) {
this.categoryList = response.data.categoryList || []
this.typeList = response.data.typeList || []
} else {
console.error(response)
}
}).finally(() => {
})
},
selectMode (ruleType) {
this.settingObj.ruleType = ruleType
this.changeEditFlag()
},
changeEditFlag () {
this.settingObj.editFlag = true
},
/** 点击继续,进行第二步 */
onContinue () {
this.$refs.form.validate(valid => {
if (valid) {
this.settingObj.editFlag = false
this.settingObj.saveFlag = true
this.$emit('setSettingForm', this.settingObj)
}
})
}
}
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,234 @@
<template>
<div class="history-top-key" v-if="myDrawer">
<el-drawer v-model="myDrawer" :with-header="false">
<div class="key-header">
<div>History Top Keys</div>
<i class="cn-icon cn-icon-close" @click="closeDrawer"></i>
</div>
<div class="key-search">
<el-input v-model="searchKey" @keyup.enter="onSearch" size="mini" placeholder="Search for">
<template #prefix>
<!--todo 该图标名称错误已在iconfont修改后续记得改过来-->
<i class="cn-icon cn-icon-serach key-search-icon"></i>
</template>
</el-input>
<i class="cn-icon cn-icon-refresh1 key-refresh" @click="onRefresh"></i>
</div>
<div class="key-total">
Total: <span class="key-total-number">{{ tableTotal }}</span>
</div>
<div class="key-table">
<loading :loading="loading"></loading>
<el-table :data="tableData" style="width: 100%" @row-click="rowClick">
<el-table-column
v-for="(item, index) in tableTitle"
:key="`col-${index}`"
:fixed="item.fixed"
:label="item.label"
:min-width="`${item.minWidth}`"
:prop="item.prop"
:sort-orders="['ascending', 'descending']"
:width="`${item.width}`"
>
<template #header>
<span>{{ item.label }}</span>
</template>
<template #default="scope" :column="item">
<template v-if="item.prop === 'metric'">
<span>{{unitConvert(scope.row.metric, unitTypes.byte).join(' ')}}</span>
</template>
<template v-else-if="item.prop === 'last'">
<span>{{dateFormatByAppearance(scope.row[item.prop])}}</span>
</template>
<span v-else>{{ scope.row[item.prop] || '-' }}</span>
</template>
</el-table-column>
<template v-slot:empty >
<chart-error v-if="showError" :content="errorMsg" style="line-height: 0" />
</template>
</el-table>
</div>
</el-drawer>
</div>
</template>
<script>
import unitConvert from '@/utils/unit-convert'
import { unitTypes } from '@/utils/constants'
import { dateFormatByAppearance } from '@/utils/date-util'
import { get } from '@/utils/http'
import { api } from '@/utils/api'
import Loading from '@/components/common/Loading'
import ChartError from '@/components/common/Error'
export default {
name: 'HistoryTopKeys',
props: {
showDrawer: {
type: Boolean,
default: false
}
},
components: {
ChartError,
Loading
},
data () {
return {
myDrawer: false,
searchKey: '',
unitTypes,
loading: false,
tableTotal: 0,
tableData: [],
tableTitle: [
{
// label: this.$t('knowledge.status'),
label: 'Keys',
prop: 'keys',
show: true,
minWidth: 120
},
{
label: 'Last Seen',
prop: 'last',
show: true,
minWidth: 150
},
{
label: 'Primary metric',
prop: 'metric',
show: true,
minWidth: 120
}
],
showError: false,
errorMsg: ''
}
},
mounted () {
this.myDrawer = this.showDrawer
this.getTopKeysData()
},
methods: {
unitConvert,
dateFormatByAppearance,
getTopKeysData (data) {
this.loading = true
const params = {}
if (data) {
// todo 具体入参按文档要求
params.param = data
}
get(api.detection.create.topKeys, params).then(res => {
this.tableTotal = 0
this.tableData = []
if (res.code === 200) {
this.tableTotal = res.data.total
this.tableData = res.data.list
} else {
this.httpError(res)
}
}).catch(err => {
this.httpError(err)
}).finally(() => {
this.loading = false
})
},
/** 关闭topKeys弹窗 */
closeDrawer () {
this.myDrawer = false
this.$emit('closeDrawer', false)
},
/** 单击topKeys弹窗某一项 */
rowClick (data) {
this.$emit('keyRowClick', data)
},
onRefresh () {
this.getTopKeysData()
},
onSearch () {
this.getTopKeysData(this.searchKey)
},
httpError (e) {
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
}
}
}
</script>
<style lang="scss">
.history-top-key {
width: 396px;
height: 520px;
.el-drawer__body {
border: 1px #E2E5EC solid;
}
}
.key-header {
height: 41px;
background: #F7F7F7;
box-shadow: 0 1px 0 0 rgba(226,229,236,1);
border-radius: 2px 2px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
i {
font-size: 12px;
color: #575757;
cursor: pointer;
}
}
.key-search {
margin-left: 12px;
margin-top: 6px;
display: flex;
align-items: center;
justify-content: space-between;
.key-search-icon {
font-size: 13px;
color: #999999;
}
.key-refresh {
margin: 0 10px;
color: #38ACD2;
font-size: 14px;
height: 28px;
line-height: 28px;
cursor: pointer;
}
}
.key-total {
margin: 10px 12px;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #999999;
line-height: 21px;
font-weight: 400;
.key-total-number {
color: #666666;
margin-left: 4px;
}
}
</style>

View File

@@ -0,0 +1,488 @@
<template>
<div>
<div v-if="mySettingObj.ruleType===detectionRuleType.threshold" style="display: flex;justify-content: space-between;">
<div>
<el-form ref="form" :model="thresholdRuleObj" label-position="top" :rules="rules">
<!--source-->
<el-form-item label="Source" prop="dataSource" class="form-setting__block margin-b-20">
<el-select v-model="thresholdRuleObj.dataSource" placeholder=" " size="mini" class="form-setting__select">
<el-option
v-for="item in sourceList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<!--Dimensions-->
<div class="form-setting__block margin-b-20">
<div class="block-title">Dimensions</div>
<div class="block-dimension">
<div class="block-dimension-tag" v-for="(ite, ind) in dimensionList" :key="ind">{{ ite.label }}</div>
</div>
</div>
<div class="form-setting__block form-setting__block-key">
<div>Key Selection</div>
<div class="block-key">
<!--todo 图标暂无需要更换-->
<i class="cn-icon cn-icon-shijianjihua"></i>
<span @click="showDrawer=true">History Top Keys</span>
</div>
</div>
<!--Filters模块-->
<div class="form-setting__block margin-b-20">
<div class="block-title1">{{ $t('detections.filters') }}</div>
<div class="definition-filter-block" v-if="showFilter">
<div class="definition-filter-item" v-for="(item, index) in thresholdRuleObj.filterList" :key="index">
<el-select class="filter-item__select margin-r-8" v-model="item.filter" placeholder=" " size="mini">
<el-option
v-for="item in selectList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input class="filter-item__input margin-r-8" size="mini" disabled placeholder="equal"></el-input>
<el-input
class="filter-item__input margin-r-8"
size="mini"
oninput="value=value.replace(/[^\d]/g,'')"
v-model="item.value"></el-input>
<i class="cn-icon cn-icon-close" @click="delFilterItem(index)"></i>
</div>
<div style="height: 10px;"></div>
<div class="filter-block-footer">
<i class="cn-icon cn-icon-add" @click="addFilter"></i>
</div>
</div>
<div v-else class="block-filter-add" @click="addFilter">+</div>
</div>
<!--Condition模块-->
<div class="form-setting__block margin-b-20">
<div class="block-title">Condition</div>
<el-form ref="form2" :model="thresholdRuleObj" label-position="top">
<div class="definition-condition-block" v-for="(item, index) in thresholdRuleObj.conditionData" :key="index">
<el-form-item :label="$t('detection.level')" :prop="`conditionData.${index}.level`" :rules="rules.level">
<el-select class="condition__select margin-b-20" v-model="item.level" placeholder=" " size="mini">
<template #prefix>
<div
class="condition__select__icon"
:style="{background: eventSeverityColor[item.level]}"></div>
</template>
<el-option
v-for="item in levelList"
:key="item.label"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
/>
</el-select>
</el-form-item>
<div class="condition-metric" v-for="(data, i) in item.list" :key="i">
<div class="condition-metric-item1">
<div class="metric-item1__text">
<span style="margin-right: 9px;">If</span>
<el-form-item :prop="`conditionData.${index}.list.${i}.metric`" :rules="rules.metric">
<el-select v-model="data.metric" placeholder=" " size="mini">
<el-option
v-for="item in metricList"
:key="item.label"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<span style="margin-left: 9px;">of keys</span>
</div>
<div>
<i class="cn-icon cn-icon-add" @click="addConditionItem(index)"></i>
<i
class="cn-icon cn-icon-close"
:class="delConditionDisabled ? 'metric-item1-close-disable' : 'metric-item1-close'"
@click="delConditionItem(index, i)">
</i>
</div>
</div>
<div class="condition-metric-item2">
<div style="height: 24px;line-height: 24px;display: flex;">
<el-form-item :prop="`conditionData.${index}.list.${i}.condition`" :rules="rules.condition">
<el-select class="metric-item2__select" v-model="data.condition" placeholder=" " size="mini">
<el-option
v-for="item in conditionList"
:key="item.label"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :prop="`conditionData.${index}.list.${i}.value`" :rules="rules.value">
<!--todo 应当添加长度限制-->
<el-input
class="metric-item2__input"
v-model.number="data.value"
oninput="value=value.replace(/[^\d]/g,'')"
size="mini"></el-input>
</el-form-item>
</div>
<span>{{ data.metric }}</span>
</div>
<el-divider v-if="item.list.length - 1 > i" class="condition-divider">and</el-divider>
</div>
</div>
</el-form>
<div class="condition-add" @click="addCondition">
<i class="cn-icon cn-icon-add"></i>Add Condition
</div>
</div>
</div>
<!--History Top Keys弹窗-->
<div class="key-drawer">
<history-top-keys
v-if="showDrawer"
:showDrawer="showDrawer"
@closeDrawer="onCloseDrawer"
@keyRowClick="getRowClick"
></history-top-keys>
</div>
</div>
<div v-if="mySettingObj.ruleType===detectionRuleType.indicator">
<el-form ref="form" :model="indicatorRuleObj" label-position="top" :rules="rules">
<!--Source-->
<el-form-item label="Source" prop="dataSource" class="form-setting__block margin-b-20">
<el-select v-model="indicatorRuleObj.dataSource" class="form-setting__select" placeholder=" " size="mini">
<el-option
v-for="item in sourceList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!--Library-->
<el-form-item label="Library" prop="knowledgeId" class="form-setting__block margin-b-20">
<el-select v-model="indicatorRuleObj.knowledgeId" class="form-setting__select" placeholder=" " size="mini">
<el-option
v-for="item in libraryList"
:key="item.knowledgeId"
:label="item.label"
:value="item.knowledgeId"
/>
</el-select>
</el-form-item>
<!--Level-->
<el-form-item :label="$t('detection.level')" prop="level" class="form-setting__block">
<el-select v-model="indicatorRuleObj.level" class="condition__select form-setting__select" placeholder=" " size="mini">
<template #prefix>
<div
class="condition__select__icon"
:style="{background: eventSeverityColor[indicatorRuleObj.level]}"></div>
</template>
<el-option
v-for="item in levelList"
:key="item.label"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="form-setting__btn">
<el-button @click="onContinue">Continue</el-button>
</div>
</div>
</template>
<script>
import { get } from '@/utils/http'
import { api } from '@/utils/api'
import HistoryTopKeys from '@/components/table/detection/HistoryTopKeys'
import { eventSeverityColor, detectionRuleType } from '@/utils/constants'
export default {
name: 'RuleDefinition',
props: {
settingObj: {
type: Object
}
},
components: {
HistoryTopKeys
},
watch: {
settingObj: {
immediate: true,
deep: true,
handler (newVal, oldVal) {
if (!newVal.editFlag && newVal.saveFlag) {
this.mySettingObj = JSON.parse(JSON.stringify(newVal))
}
}
}
},
data () {
return {
eventSeverityColor,
detectionRuleType,
mySettingObj: {
ruleType: detectionRuleType.threshold
},
dimensionList: [], // Dimensions数据
// ruleType为Indicator时表单数据
indicatorRuleObj: {
dataSource: '',
knowledgeId: '',
level: ''
},
// ruleType为Threshold时表单数据
thresholdRuleObj: {
dataSource: '',
dimensionKeys: '',
filterList: [],
filters: '', // filter提交到接口的数据即filterList转化为字符串
conditionData: [
{
level: '',
list: [
{ metric: '', condition: '', value: '' }
]
}
],
conditions: {} // filter提交到接口的数据即filterList转化为字符串
},
rules: {
dataSource: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
level: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
metric: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
condition: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
value: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'blur'
}
],
knowledgeId: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'blur'
}
]
},
sourceList: [], // source下拉列表数据
// rule的policy创建信息
showDrawer: false, // 显示History Top Keys抽屉弹框
showFilter: false, // 显示filter筛选框点击add显示
selectList: [], // filter的第一个下拉列表
levelList: [], // condition的level下拉列表
metricList: [],
conditionList: [],
libraryList: [],
delConditionDisabled: true, // condition删除标识true表示禁止删除即只有一个condition时
unitObj: {
than: '>',
less: '<',
equal: '='
}
}
},
mounted () {
this.initData()
// todo 调用接口进行赋值
this.dimensionList = [
{ label: 'Destination IP/CIDR', value: 'Destination IP/CIDR' },
{ label: 'Source Port Number', value: 'Source Port Number' }
]
},
methods: {
initData () {
get(api.detection.statistics, { pageSize: -1 }).then(response => {
if (response.code === 200) {
this.sourceList = response.data.sourceList || []
this.levelList = response.data.levelList || []
this.conditionList = response.data.conditionList || []
this.metricList = response.data.metricList || []
this.libraryList = response.data.libraryList || []
} else {
console.error(response)
}
}).finally(() => {
})
},
/** 单击History Top Keys列表某行filter添加数据 */
getRowClick (data) {
this.addFilter(data)
},
/** 关闭History Top Keys弹框 */
onCloseDrawer () {
this.showDrawer = false
},
/** filter模块点击add按钮 */
addFilter (data) {
this.showFilter = true
if (this.selectList.length === 0) {
this.getFilterList()
}
// 添加数据前,先删除空白项
const delIndex = this.thresholdRuleObj.filterList.findIndex(t => t.filter === '')
if (delIndex > -1) {
this.thresholdRuleObj.filterList.splice(delIndex, 1)
}
if (data.metric) {
// 从key弹框添加数据
const obj = this.thresholdRuleObj.filterList.find(t => t.keyId === data.keyId)
if (!obj) {
this.thresholdRuleObj.filterList.push({ keyId: data.keyId, filter: this.selectList[0].label, value: data.metric })
}
} else {
// 手动添加
this.thresholdRuleObj.filterList.push({ filter: '', value: '' })
}
},
/** 添加condition大模块 */
addCondition () {
this.$refs.form2.validate(valid => {
if (valid) {
// 如果选择了level则禁用这一条level不允许再选择除非删除才能选择
this.levelList.forEach(item => {
const obj = this.thresholdRuleObj.conditionData.find(t => t.level === item.value)
if (obj) {
item.disabled = true
}
})
this.thresholdRuleObj.conditionData.push({ level: '', list: [{ metric: '', condition: '', value: '' }] })
this.delConditionDisabled = false
}
})
},
/** 添加condition小模块 */
addConditionItem (index) {
this.$refs.form2.validate(valid => {
if (valid) {
this.thresholdRuleObj.conditionData[index].list.push({ metric: '', condition: '', value: '' })
this.delConditionDisabled = false
}
})
},
/**
* 删除condition小模块只剩一个时点击删除则删除大模块
* 只有一个大模块时,只剩一个小模块则不可删除
* */
delConditionItem (index, i) {
if (!this.delConditionDisabled) {
const dataLen = this.thresholdRuleObj.conditionData.length
const listLen = this.thresholdRuleObj.conditionData[index].list.length
if (dataLen === 1) {
if (listLen >= 2) {
this.thresholdRuleObj.conditionData[index].list.splice(i, 1)
}
if (this.thresholdRuleObj.conditionData[index].list.length === 1) {
this.delConditionDisabled = true
}
} else {
if (listLen === 1) {
this.thresholdRuleObj.conditionData.splice(index, 1)
} else {
this.thresholdRuleObj.conditionData[index].list.splice(i, 1)
}
}
}
},
/** 获取filter模块下拉列表 */
getFilterList () {
// todo 请求接口
this.selectList = [
{ value: 'Destination As Number', label: 'Destination As Number' },
{ value: 'Destination As String', label: 'Destination As String' }
]
},
/** 删除filter某一项 */
delFilterItem (i) {
this.thresholdRuleObj.filterList.splice(i, 1)
},
/** 点击继续,展开第三步 */
onContinue () {
this.$refs.form.validate(valid => {
if (valid) {
if (this.mySettingObj.ruleType === detectionRuleType.indicator) {
// 第一步模式选择Indicator Match
this.$emit('setRuleObj', this.indicatorRuleObj)
} else {
// 第一步模式选择Threshold
this.$refs.form2.validate(valid2 => {
if (valid2) {
this.getConditions()
this.$emit('setRuleObj', this.thresholdRuleObj)
}
})
}
}
})
},
/** 将condition的数组转换为入参需要的字符串 */
getConditions () {
const conditionData = this.thresholdRuleObj.conditionData
const obj = {}
conditionData.forEach(item => {
obj[item.level] = ''
let str = ''
item.list.forEach(t => {
str = str + t.metric + ' ' + this.unitObj[t.condition] + ' ' + t.value + ' && '
})
str = str.substring(0, str.length - 4)
obj[item.level] = str
})
this.thresholdRuleObj.conditions = obj
}
}
}
</script>

View File

@@ -118,6 +118,43 @@ export default {
if (this.listUrl) {
listUrl = this.listUrl
}
// todo 此段是为了避免mock没开启打开detection界面报错提示后续再开发detection时删除
if (listUrl === api.detection.list) {
const list = []
for (let i = 0; i < 20; i++) {
const obj = {
ruleId: 100000 + i,
ruleType: 'indicator_match',
status: 1,
name: 'name123',
category: 'Security Event',
eventType: 'C&C',
description: 'Built-in darkweb IoC',
ruleConfig: {
knowledge: {
name: 'VPN Server IP',
category: 'user_defined'
}
}
}
if (i % 2 === 0) {
obj.ruleType = 'threshold'
obj.ruleConfig = {
dimensions: 'Destination IP/CIDR'
}
obj.description = 'abuse.ch is providing community driven threat intelligence on \n' +
'cyber threats. It is the home of a couple of projects that are \n' +
'helping internet service providers and network operators protect …'
} else {
obj.status = 0
}
list.push(obj)
}
this.tableData = list
this.pageObj.total = list.length
this.loading = false
} else {
get(listUrl, this.searchLabel).then(response => {
if (response.code === 200) {
this.tableData = response.data.list
@@ -140,7 +177,9 @@ export default {
this.isNoData = true
}).finally(() => {
this.toggleLoading(false)
this.loading = false
})
}
},
del (row) {
this.$confirm(this.$t('tip.confirmDelete'), {
@@ -396,7 +435,7 @@ export default {
tableData: {
deep: true,
handler (n) {
if (n.length === 0 && this.pageObj.pageNo > 1) {
if (n && n.length === 0 && this.pageObj.pageNo > 1) {
this.pageNo(this.pageObj.pageNo - 1)
}
// TODO 不是删除时回到顶部

167
src/mock/detection.js Normal file
View File

@@ -0,0 +1,167 @@
import Mock from 'mockjs'
const urlAndVersion = BASE_CONFIG.baseUrl + BASE_CONFIG.apiVersion
const openMock = true
if (openMock) {
Mock.mock(new RegExp(urlAndVersion + '/rule/detection/list.*'), 'get', function (requestObj) {
const list = []
for (let i = 0; i < 20; i++) {
const obj = {
ruleId: 100000 + i,
ruleType: 'indicator_match',
status: 1,
name: 'name123',
category: 'Security Event',
eventType: 'C&C',
description: 'Built-in darkweb IoC',
ruleConfig: {
knowledge: {
name: 'VPN Server IP',
category: 'user_defined'
}
}
}
if (i % 2 === 0) {
obj.ruleType = 'threshold'
obj.ruleConfig = {
dimensions: 'Destination IP/CIDR'
}
obj.description = 'abuse.ch is providing community driven threat intelligence on \n' +
'cyber threats. It is the home of a couple of projects that are \n' +
'helping internet service providers and network operators protect …'
} else {
obj.status = 0
}
list.push(obj)
}
const data = {
total: list.length,
pageSize: 20,
pageNo: 1,
list: list
}
return {
msg: 'success',
code: 200,
data: data
}
})
Mock.mock(new RegExp(urlAndVersion + '/detection/statistics.*'), 'get', function (requestObj) {
const data = {
statusList: [
{ status: 1 },
{ status: 0 }
],
categoryList: [
{ value: 'security', label: 'Security Event' },
{ value: 'performance', label: 'Performance Event' },
{ value: 'regulatory_risk', label: 'Regulatory Risk Event' }
],
typeList: [
{ value: 'c&c', label: 'C&C' },
{ value: 'ddos', label: 'DDos' },
{ value: 'lateral_movement', label: 'Lateral movement' },
{ value: 'brute_force', label: 'Brute force' }
],
sourceList: [
{ value: 'ip_metric', label: 'IP metric' },
{ value: 'performance_event', label: 'performance event' }
],
levelList: [
{ value: 'critical', label: 'Critical' },
{ value: 'high', label: 'High' },
{ value: 'medium', label: 'Medium' },
{ value: 'low', label: 'Low' },
{ value: 'info', label: 'Info' }
],
metricList: [
{ value: 'tcp_lostlen_ratio', label: 'Bits/second' },
{ value: 's2c_byte_retrans_ratio', label: 'Packets/second' },
{ value: 's2c_byte_retrans_ratio1', label: 'Sessions/second' }
],
conditionList: [
{ value: 'than', label: 'Greater Than' },
{ value: 'less', label: 'Greater Less' },
{ value: 'equal', label: 'Greater Equal' }
],
libraryList: [
{ value: 'library name2', knowledgeId: '101', label: 'Library name' },
{ value: 'library name1', knowledgeId: '102', label: 'Library name1' },
{ value: 'library name2', knowledgeId: '103', label: 'Library name2' }
],
intervalList: [
{ value: 'minutes', label: 'minutes' },
{ value: 'hours', label: 'hours' },
{ value: 'days', label: 'days' },
{ value: 'weeks', label: 'weeks' }
]
}
return {
msg: 'success',
code: 200,
data: data
}
})
Mock.mock(new RegExp(urlAndVersion + '/detection/topKeys.*'), 'get', function (requestObj) {
const list = [
{ keyId: '10000001', keys: '192.168.40.54', last: 1690266188, metric: 181440000000 },
{ keyId: '10000002', keys: '192.168.40.55', last: 1690266188, metric: 161440000000 },
{ keyId: '10000003', keys: '192.168.40.56', last: 1690266188, metric: 181440000000 },
{ keyId: '10000004', keys: '192.168.40.57', last: 1690266188, metric: 171440000000 },
{ keyId: '10000005', keys: '192.168.40.58', last: 1690266188, metric: 171440000000 },
{ keyId: '10000006', keys: '192.168.40.59', last: 1690266188, metric: 187440000000 },
{ keyId: '10000007', keys: '192.168.40.60', last: 1690266188, metric: 181440000000 }
]
const data = {
list: list,
total: list.length
}
return {
msg: 'success',
code: 200,
data: data
}
})
Mock.mock(new RegExp(urlAndVersion + '/rule/detection.*'), 'get', function (requestObj) {
const ruleId = getLastValue(requestObj.url)
const data = {
name: 'name123',
category: 'Security Event',
ruleType: 'indicator_match',
eventType: 'C&C',
description: 'Built-in darkweb IoC',
status: 1,
ruleConfig: {
dataSource: 'VPN Server IP',
knowledgeId: 10,
level: 10
},
trigger: {
atLeast: 1,
interval: 'PT5M',
resetInterval: 'PT10M'
}
}
if (ruleId % 2 === 0) {
data.ruleType = 'threshold'
data.status = 1
} else {
data.status = 0
}
return {
msg: 'success',
code: 200,
data: data
}
})
}
const getLastValue = (url) => {
const index = url.lastIndexOf('/')
return url.substring(index + 1)
}

View File

@@ -2,3 +2,4 @@ import './npm'
import './linkMonitor'
import './dns'
import './entity'
import './detection'

View File

@@ -96,6 +96,18 @@ const routes = [
name: 'Chart',
path: '/chart',
component: () => import('@/views/administration/Chart')
},
{
path: '/detectionsNew',
component: () => import('@/views/detectionsNew/Index')
},
{
path: '/detection/policies',
component: () => import('@/views/detectionsNew/Index')
},
{
path: '/detection/policies/create',
component: () => import('@/views/detectionsNew/DetectionForm')
}
]
}

View File

@@ -136,6 +136,16 @@ export const api = {
dnsErrorMetric: '/interface/detection/performance/detail/overview/metric/dnsError',
httpErrorMetric: '/interface/detection/performance/detail/overview/metric/httpError',
highDnsResponseTimeMetric: '/interface/detection/performance/detail/overview/metric/highDnsResponseTime'
},
list: apiVersion + '/rule/detection/list', // 检测规则列表
detail: apiVersion + '/rule/detection', // 检测规则详情
delete: apiVersion + '/rule', // 检测规则删除
// 获取单位列表如source、type、metric等
statistics: apiVersion + '/detection/statistics',
// 规则新建模块
create: {
topKeys: apiVersion + '/detection/topKeys', // topKeys列表
create: apiVersion + '/rule/detection/create' // todo 规则新建编辑此api为模拟后续需要修改
}
},
// Dashboard

File diff suppressed because one or more lines are too long

View File

@@ -1300,3 +1300,17 @@ export function numberWithCommas (num) {
return num
}
}
/**
* 根据状态编码01转化为DisabledEnabled
* @param status
* @returns {string}
*/
export function switchStatus (status) {
switch (status) {
case 0:
return 'Disabled'
case 1:
return 'Enabled'
}
}

View File

@@ -0,0 +1,173 @@
<template>
<div class="detection-drawer" style="height: 100vh;overflow: scroll;padding-bottom: 90px">
<div class="drawer-basic">
<div class="drawer-basic-header">
<div class="drawer-basic-id">ID: {{ drawerInfo.ruleId }}</div>
<div :class="`detection-tag-status${detailData.status}`">
{{ switchStatus(detailData.status) }}
</div>
</div>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Name</div>
<div class="basic-function-value">{{ detailData.name }}</div>
</div>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Type</div>
<div class="basic-function-value">{{ detailData.eventType }}</div>
</div>
<div class="drawer-basic-description">
<div class="detection-drawer-title">Description</div>
<div class="basic-description-value">{{ detailData.description }}</div>
</div>
</div>
<div class="detection-drawer-collapse">
<el-collapse v-model="activeRule">
<el-collapse-item title="Rule Definitcm" name="rule">
<div class="drawer-collapse-content">
<div class="drawer-basic-function">
<div class="detection-drawer-title">Source</div>
<div class="basic-function-value">{{ detailData.category }}</div>
</div>
<div v-if="detailData.ruleType==='indicator_match'">
<div class="drawer-basic-function">
<div class="detection-drawer-title">Library</div>
<span class="basic-function-value">{{ detailData.library }}</span>
</div>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Level</div>
<div class="detection-drawer-title">
<div class="detection__icon" :style="`background-color: ${eventSeverityColor['critical']}`"></div>
<div class="basic-function-value">Critical</div>
</div>
</div>
</div>
<div v-else>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Dimensions</div>
<span class="detection-tag-blue">{{ detailData.dimensions }}</span>
</div>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Filters</div>
<span class="detection-tag-blue">source</span>
<span style="margin: 0 6px;">equal</span><span>19890</span>
</div>
<div class="drawer-basic-function" v-for="item in severityList" :key="item.severity"
style="padding-bottom: 0">
<div class="detection-drawer-title">
<div class="detection__icon" :style="`background-color: ${eventSeverityColor[item.severity]}`"></div>
<div>{{ toUpperCaseByString(item.severity) }}</div>
</div>
<div class="detection-drawer-title">Conditions</div>
<div>
<div class="detection-tag-gray margin-r-10">> 60 Kpackets/s</div>
<div class="detection-tag-gray">> 50 Unique Src IPs</div>
</div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<div class="detection-drawer-collapse" style="margin: 20px 0">
<el-collapse v-model="activeTrigger">
<el-collapse-item title="Trigger" name="trigger">
<div class="drawer-collapse-content">
<div class="drawer-collapse-trigger">
Triggered when conditions occur at least
<span style="color: #046ECA" v-if="detailData.trigger">
{{ detailData.trigger.atLeast }} time
</span> in
<span style="color: #046ECA" v-if="detailData.trigger">
<!--todo 此处返回的是PT5M具体时间处理根据后续字段来看-->
{{ getNumberFromStr(detailData.trigger.interval) }} minutes
</span>
</div>
<div class="drawer-basic-function">
<div class="detection-drawer-title">Evaluation Frequency</div>
<div class="basic-function-value" v-if="detailData.trigger">{{ getNumberFromStr(detailData.trigger.resetInterval) }} minutes</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
import { switchStatus, toUpperCaseByString } from '@/utils/tools'
import { eventSeverityColor } from '@/utils/constants'
import axios from '_axios@0.21.4@axios'
import { api } from '@/utils/api'
export default {
name: 'DetectionDrawer',
props: {
drawerInfo: {
type: Object
}
},
data () {
return {
activeRule: 'rule',
activeTrigger: 'trigger',
detailData: {},
eventSeverityColor,
severityList: []
}
},
watch: {
drawerInfo: {
immediate: true,
deep: true,
handler (n) {
if (n) {
this.getDetailData()
}
}
}
},
methods: {
switchStatus,
toUpperCaseByString,
getDetailData () {
this.severityList = [
{
severity: 'critical',
list: ['> 60 Kpackets/s', '> 50 Unique Src IPs']
},
{
severity: 'high',
list: ['> 20 Kpackets/s', '> 50 Unique Src IPs']
}
]
axios.get(`${api.detection.detail}/${this.drawerInfo.ruleId}`).then(res => {
if (res.data.code === 200) {
this.detailData = res.data.data
}
}).catch(err => {
console.error(err)
})
},
getNumberFromStr (str) {
return str.match(/\d+(\.\d+)?/g)[0]
}
}
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,215 @@
<template>
<div>
<div class="detection-filter-title">
{{ $t('detections.filters') }}
</div>
<div class="detection-filter-content">
<div>
<div class="filter-content-title">{{ $t('overall.status') }}</div>
<div class="filter-content-content">
<el-checkbox-group v-model="checkStatus" @change="onChangeCategory" style="display: flex;flex-direction: column">
<el-checkbox v-for="item in statusList" :key="item.label" class="filter-content-checkbox" :label="item.status">
<div>{{ item.label }}</div>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div>
<div class="filter-content-title">{{ $t('overall.category') }}</div>
<div class="filter-content-content">
<el-checkbox-group v-model="checkCategory" @change="onChangeCategory">
<el-checkbox v-for="item in categoryList" :key="item.value" class="filter-content-checkbox" :label="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div>
<div class="filter-content-title">{{ $t('overall.type') }}</div>
<div class="filter-content-content">
<el-checkbox-group v-model="checkType" @change="onChangeCategory" style="display: flex;flex-direction: column">
<el-checkbox v-for="item in typeList" :key="item.value" class="filter-content-checkbox" :label="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</template>
<script>
import { get } from '@/utils/http'
import { api } from '@/utils/api'
export default {
name: 'DetectionFilter',
data () {
return {
statusList: [],
categoryList: [],
typeList: [],
checkStatus: [],
checkCategory: [],
checkType: [],
url: api.detection.statistics
}
},
mounted () {
// 开发时删除---start
this.statusList = [
{ status: 1, label: 'Enabled' },
{ status: 0, label: 'Disabled' }
]
this.checkStatus = [0, 1]
this.categoryList = [
{ value: 'security', label: 'Security Event' },
{ value: 'performance', label: 'Performance Event' },
{ value: 'regulatory_risk', label: 'Regulatory Risk Event' }
]
this.checkCategory = ['security', 'performance', 'regulatory_risk']
this.typeList = [
{ value: 'c&c', label: 'C&C' },
{ value: 'ddos', label: 'DDos' },
{ value: 'lateral_movement', label: 'Lateral movement' },
{ value: 'brute_force', label: 'Brute force' }
]
this.checkType = ['c&c', 'ddos', 'lateral_movement', 'brute_force']
// 开发时删除---end
// todo 暂时禁用,后续再开发时解禁
// this.getFilterData()
},
methods: {
getFilterData (params) {
let searchParams = { pageSize: -1 }
if (params) {
searchParams = { ...searchParams, ...params }
}
get(this.url, searchParams).then(response => {
if (response.code === 200) {
if (response.data.statusList) {
this.statusList = []
response.data.statusList.forEach(item => {
this.statusList.push({ status: item.status, label: this.switchStatus(item.status) })
this.checkStatus.push(item.status)
})
}
this.categoryList = response.data.categoryList
if (response.data.categoryList) {
response.data.categoryList.forEach(item => {
this.checkCategory.push(item.value)
})
}
this.typeList = response.data.typeList
if (response.data.typeList) {
response.data.typeList.forEach(item => {
this.checkType.push(item.value)
})
}
} else {
console.error(response)
}
}).finally(() => {
// this.initTypeData()
// this.initStatusData()
// const self = this
// this.$nextTick(() => {
// if (self.$refs.knowledgeTreeTypeFilter) {
// self.$refs.knowledgeTreeTypeFilter.setCheckedKeys(this.defaultCheckedCategory)
// }
// if (self.$refs.knowledgeTreeStatusFilter) {
// self.$refs.knowledgeTreeStatusFilter.setCheckedKeys(this.defaultCheckedStatus)
// }
// })
})
},
onChangeCategory (data) {
// todo 暂时禁用,后续再开发时解禁
// 根据选择的值,构造不同入参,传给列表页,调用查询列表接口
},
switchStatus (status) {
switch (status) {
case 0: return 'Disabled'
case 1: return 'Enabled'
}
}
}
}
</script>
<style lang="scss" scoped>
.detection-filter-title {
height: 32px;
line-height: 32px;
background: #F7F7F7;
padding: 0 20px;
box-shadow: 0 1px 0 0 rgba(226, 229, 236, 1);
border-radius: 4px 4px 0 0;
}
.detection-filter-content {
padding: 20px;
.filter-content-title {
font-family: NotoSansHans-Medium;
font-size: 14px;
line-height: 14px;
margin-bottom: 10px;
color: #353636;
font-weight: 500;
}
.filter-content-content {
display: flex;
flex-direction: column;
.filter-content-checkbox {
line-height: 16px;
margin-bottom: 10px;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
.el-checkbox__inner {
width: 16px !important;
height: 16px !important;
text-align: center !important;
line-height: 16px !important;
}
.el-checkbox__input.is-indeterminate .el-checkbox__inner {
border-color: #38ACD2;
background: #38ACD2;
border-radius: 2px;
}
.el-checkbox__input.is-indeterminate .el-checkbox__inner:before {
background: #FFFFFF;
border-radius: 1px;
}
.el-checkbox__input.is-checked {
.el-checkbox__inner {
border-color: #38ACD2;
background: #38ACD2;
border-radius: 2px;
}
}
.el-checkbox__input.is-focus {
.el-checkbox__inner {
border-color: #38ACD2;
}
}
.el-checkbox__label {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
}
}
}
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<div class="detection-form">
<div class="detection-form-header">
Create Alert Policies
</div>
<!--第一步General Settings-->
<div class="detection-form-content">
<div class="detection-form-collapse">
<el-collapse v-model="activeNames">
<el-collapse-item name="1">
<template #title>
<div class="form-collapse-header">
<div :class="activeNames[0]==='1' ? 'form-collapse-header-no-active' : 'form-collapse-header-no'">1</div>
<div class="form-collapse-header-title">General Settings</div>
</div>
</template>
<div class="form-collapse-content">
<general-settings ref="form" @setSettingForm="getFormSetting" />
</div>
</el-collapse-item>
</el-collapse>
</div>
<!--第二步Rule Definition-->
<div class="detection-form-collapse">
<el-collapse v-model="activeNames" style="position: relative;">
<el-collapse-item name="2">
<template #title>
<div class="form-collapse-header">
<div :class="activeNames[0]==='2' ? 'form-collapse-header-no-active' : 'form-collapse-header-no'">2</div>
<div class="form-collapse-header-title">Rule Definition</div>
</div>
</template>
<div class="form-collapse-content">
<rule-definition :settingObj="settingObj" @setRuleObj="getRuleObj" />
</div>
</el-collapse-item>
</el-collapse>
</div>
<!--第三步Trigger-->
<div class="detection-form-collapse">
<el-collapse v-model="activeNames">
<el-collapse-item name="3">
<template #title>
<div class="form-collapse-header">
<div :class="activeNames[0]==='3' ? 'form-collapse-header-no-active' : 'form-collapse-header-no'">3</div>
<div class="form-collapse-header-title">Trigger</div>
</div>
</template>
<div class="form-collapse-content margin-t-18">
<el-form class="trigger-block margin-b-20" ref="form3" :model="triggerObj" :rules="rules">
<div class="trigger-block-item margin-b-10">
<div>At least</div>
<el-form-item prop="atLeast">
<el-input size="mini" v-model="triggerObj.atLeast" oninput="value=value.replace(/[^\d]/g,'')"></el-input>
</el-form-item>
<div>times within</div>
<el-form-item prop="interval">
<el-input size="mini" v-model="triggerObj.interval" oninput="value=value.replace(/[^\d]/g,'')"></el-input>
</el-form-item>
<el-form-item prop="intervalVal">
<el-select v-model="triggerObj.intervalVal" class="form-trigger__select" placeholder=" " size="mini">
<el-option
v-for="item in intervalList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="trigger-block-item">
<div>With the counter resetting after no activity for</div>
<el-form-item prop="minute">
<el-input size="mini" v-model="triggerObj.minute" oninput="value=value.replace(/[^\d]/g,'')"></el-input>
</el-form-item>
<div>minutes</div>
</div>
</el-form>
<div class="form-setting__btn1">
<el-button @click="createPolicy">Create & enable rule</el-button>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</template>
<script>
import GeneralSettings from '@/components/table/detection/GeneralSettings'
import RuleDefinition from '@/components/table/detection/RuleDefinition'
import { get } from '@/utils/http'
import { api } from '@/utils/api'
import axios from 'axios'
export default {
name: 'DetectionForm',
data () {
return {
activeNames: ['1'],
settingObj: {}, // General Settings即第一步的form表单信息
ruleObj: {}, // 第二步的form表单信息
// 第三步的form表单信息
triggerObj: {
atLeast: '',
interval: '',
intervalVal: '',
minute: '',
finishFlag: false
},
rules: {
atLeast: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'blur'
}
],
interval: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
intervalVal: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'change'
}
],
minute: [
{
required: true,
message: this.$t('validate.required'),
trigger: 'blur'
}
]
},
intervalList: []
}
},
components: {
GeneralSettings,
RuleDefinition
},
mounted () {
this.getStatistics()
},
methods: {
/** 获取下拉列表数据 */
getStatistics () {
get(api.detection.statistics, { pageSize: -1 }).then(response => {
if (response.code === 200) {
this.intervalList = response.data.intervalList || []
} else {
console.error(response)
}
}).finally(() => {
})
},
/** 获取General Settings折叠板form数据 */
getFormSetting (data) {
this.handleActiveNames('1', this.activeNames)
this.settingObj = JSON.parse(JSON.stringify(data))
},
/** 获取Rule Definition折叠板form数据 */
getRuleObj (data) {
this.handleActiveNames('2', this.activeNames)
this.ruleObj = JSON.parse(JSON.stringify(data))
},
/** 自动展开收起折叠板 */
handleActiveNames (name, arr) {
const list = arr
list.splice(list.indexOf(name), 1)
this.activeNames = []
list.forEach(t => {
this.activeNames.push(t)
})
},
/** 创建policy */
createPolicy () {
const self = this
const settingLen = Object.keys(this.settingObj).length
const ruleLen = Object.keys(this.ruleObj).length
if (settingLen > 0 && ruleLen > 0) {
this.$refs.form3.validate(valid => {
if (valid) {
// 最终提交form
// const formObj = { ...this.settingObj, ...this.ruleObj, ...this.triggerObj }
this.$message({
duration: 2000,
type: 'success',
message: this.$t('tip.saveSuccess')
})
// axios.post('api', formObj).then(response => {
// if (response.data.code === 200) {
// this.$message({
// duration: 2000,
// type: 'success',
// message: this.$t('tip.saveSuccess')
// })
//
// this.$router.push({
// path: '/detectionNew',
// query: {
// t: +new Date()
// }
// })
// } else {
// this.$message.error(this.errorMsgHandler(response))
// }
// }).catch(e => {
// console.error(e)
// this.$message.error(this.errorMsgHandler(e))
// }).finally(() => {
// //
// })
}
})
} else if (settingLen === 0) {
this.handleFormError('1')
} else if (ruleLen === 0) {
this.handleFormError('2')
}
},
handleFormError (name) {
const list = this.activeNames
list.push(name)
this.activeNames = []
list.forEach(t => {
this.activeNames.push(t)
})
this.$message.error('请确保信息填写完整')
}
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<el-table
id="detectionTable"
class="detection-table"
ref="dataTable"
:data="tableData"
height="100%"
border
empty-text=" "
@header-dragend="dragend"
@sort-change="tableDataSort"
@selection-change="selectionChange"
@row-dblclick="rowDoubleClick"
>
<el-table-column
:resizable="false"
align="center"
type="selection"
:selectable="selectable"
width="55">
</el-table-column>
<el-table-column
v-for="(item, index) in customTableTitles"
:key="`col-${index}`"
:fixed="item.fixed"
:label="item.label"
:min-width="`${item.minWidth}`"
:prop="item.prop"
:resizable="true"
:sort-orders="['ascending', 'descending']"
:sortable="item.sortable"
:width="`${item.width}`"
class="data-column"
>
<template #header>
<span class="data-column__span">{{ item.label }}</span>
<div class="col-resize-area"></div>
</template>
<template #default="scope" :column="item">
<template v-if="item.prop === 'name'">
<template v-if="scope.row.i18n">
<span :title="scope.row[item.prop]">{{ $t(scope.row.i18n) }}</span>
</template>
<template v-else-if="scope.row.name">
<span :title="scope.row[item.prop]">{{ scope.row.name }}</span>
</template>
<template v-else>
<span>-</span>
</template>
</template>
<template v-else-if="item.prop === 'status'">
<div :class="`detection-tag-status${scope.row[item.prop]}`">
{{ switchStatus(scope.row[item.prop]) }}
</div>
</template>
<template v-else-if="item.prop === 'description'">
<div style="padding-right: 20px">{{ scope.row[item.prop] }}</div>
</template>
<template v-else-if="item.prop === 'dimensions' && scope.row[item.prop]">
<span class="detection-tag-blue">{{ scope.row[item.prop] }}</span>
</template>
<template v-else-if="item.prop === 'library' && scope.row[item.prop]">
<span class="detection-table-library">{{ scope.row[item.prop] }}</span>
</template>
<span v-else>{{ scope.row[item.prop] || '-' }}</span>
</template>
</el-table-column>
<template v-slot:empty>
<div class="table-no-data" v-if="isNoData">
<div class="table-no-data__title">{{ $t('npm.noData') }}</div>
</div>
</template>
</el-table>
</template>
<script>
import table from '@/mixins/table'
import { dateFormatByAppearance } from '@/utils/date-util'
import { switchStatus } from '@/utils/tools'
export default {
name: 'DetectionTable',
props: {
isNoData: {
type: Boolean,
default: false
}
},
mixins: [table],
data () {
return {
tableTitle: [
{
label: this.$t('knowledge.status'),
prop: 'status',
show: true,
minWidth: 70
},
{
label: 'ID',
prop: 'ruleId',
show: true,
minWidth: 65
},
{
label: this.$t('config.roles.name'),
prop: 'name',
minWidth: 90,
show: true
},
{
label: this.$t('overall.category'),
prop: 'category',
minWidth: 140,
show: true
},
{
label: this.$t('overall.type'),
prop: 'eventType',
minWidth: 145,
show: true
},
{
label: this.$t('overall.remark'),
prop: 'description',
minWidth: 205,
show: true
},
{
// label: this.$t('config.user.createTime'),
label: 'Dimensions',
prop: 'dimensions',
minWidth: 204,
show: true
},
{
// label: this.$t('config.user.createTime'),
label: 'Library',
prop: 'library',
minWidth: 204,
show: true
}
]
}
},
watch: {
tableData: {
immediate: true,
deep: true,
handler (n) {
if (n) {
n.forEach(t => {
if (t.ruleType === 'indicator_match') {
t.library = t.ruleConfig.knowledge.name
} else if (t.ruleType === 'threshold') {
t.dimensions = t.ruleConfig.dimensions
}
})
}
}
}
},
methods: {
dateFormatByAppearance,
switchStatus,
rowDoubleClick (data) {
this.$emit('rowDoubleClick', data)
}
}
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div>
<div class="top-tools__left">
<button
id="knowledge-base-add"
:title="$t('knowledgeBase.createKnowledgeBase')"
class="top-tool-btn margin-r-10 top-tool-btn--create"
@click="onCreate"
style="width:72px;">
<i class="cn-icon-xinjian cn-icon"></i>
<span>{{ $t('overall.create') }}</span>
</button>
<!-- <button-->
<!-- id="knowledge-base-edit"-->
<!-- :title="$t('knowledgeBase.editKnowledgeBase')"-->
<!-- class="top-tool-btn margin-r-10"-->
<!-- style="width:72px;">-->
<!-- <i class="cn-icon-edit cn-icon" ></i>-->
<!-- <span>{{$t('overall.edit')}}</span>-->
<!-- </button>-->
<button
:disabled="disableDelete"
id="knowledge-base-delete"
:title="$t('knowledgeBase.deleteKnowledgeBase')"
class="top-tool-btn margin-r-10"
@click="onDelete"
style="width:72px;">
<i class="cn-icon-delete cn-icon"></i>
<span>{{ $t('overall.delete') }}</span>
</button>
<div class="top-tool-search margin-l-10">
<el-input v-model="keyWord" size="small" style="height: 28px;" @keyup.enter="onSearch"></el-input>
<button
class="top-tool-btn top-tool-btn--search"
style="border-radius: 0 2px 2px 0 !important;"
@click="onSearch">
<i class="el-icon-search"></i>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetectionTools',
props: {
disableDelete: {
type: Boolean,
default: true
}
},
data () {
return {
keyWord: ''
}
},
methods: {
onSearch () {
this.$emit('search', this.keyWord)
},
onCreate () {
this.$emit('create')
},
onDelete (data) {
this.$emit('delete', data)
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,301 @@
<template>
<div class="detection">
<div class="detection-title">
<span>{{ $t('overall.detections') }}</span>
<span class="detection-title-label">
60 Polices created(200 max | 32 polices enabled(100 max)
</span>
</div>
<div class="detection-content">
<div class="detection-filter">
<detection-filter></detection-filter>
</div>
<div class="detection-block">
<detection-tools
@delete="toDelete"
@create="onCreate"
@search="onSearch"
:disableDelete="disableDelete"/>
<div class="detection-table" style="position: relative">
<loading :loading="loading"></loading>
<detection-table
ref="dataTable"
height="100%"
:api="url"
:isNoData="isNoData"
:custom-table-title="tools.customTableTitle"
:table-data="tableData"
:is-selected-status="isSelectedStatus"
:all-count="18"
@selectionChange="selectionChange"
@reload="reloadRowList"
@toggleLoading="toggleLoading"
@rowDoubleClick="onRowDoubleClick"
></detection-table>
</div>
<div class="knowledge-pagination">
<pagination ref="pagination" :page-obj="pageObj" :table-id="tableId" @pageNo='pageNo' @pageSize='pageSize'></pagination>
</div>
</div>
</div>
</div>
<el-dialog
v-model="showConfirmDialog"
:title="$t('overall.hint')"
width="480px"
custom-class="del-model-hint"
:before-close="handleClose">
<div class="dialog-message">{{ $t('knowledge.deleteDataHint') }}</div>
<el-table v-model="delItemList"
ref="delDataTable"
:data="batchDeleteObjs"
@selection-change="secondSelectionChange"
height="156px"
width="100%"
class="dialog-table"
cell-style="padding:4px 0px;font-size: 12px;color: #353636;font-weight: 400;"
header-cell-style="padding:4px 0px;background: #F5F8FA;font-size: 12px;color: #353636;font-weight: 500;">
<el-table-column :resizable="false" align="center" type="selection" width="50"></el-table-column>
<el-table-column property="ruleId" label="ID" width="70"></el-table-column>
<el-table-column property="name" label="Name"></el-table-column>
<el-table-column property="category" label="Category" width="100" :formatter="categoryFormat"></el-table-column>
<el-table-column property="function" label="Function" width="110" :formatter="sourceFormat"></el-table-column>
</el-table>
<template #footer>
<span class="dialog-footer">
<el-button @click="showConfirmDialog = false">{{ $t('overall.cancel') }}</el-button>
<el-button type="primary" @click="submit">{{ $t('tip.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<div class="detection-drawer">
<el-drawer v-model="showDrawer" :with-header="false">
<detection-drawer :drawer-info="drawerInfo"></detection-drawer>
</el-drawer>
</div>
</template>
<script>
import DetectionFilter from '@/views/detectionsNew/DetectionFilter'
import DetectionTools from '@/views/detectionsNew/DetectionTools'
import DetectionTable from '@/views/detectionsNew/DetectionTable'
import { api } from '@/utils/api'
import dataListMixin from '@/mixins/data-list'
import DetectionDrawer from '@/views/detectionsNew/DetectionDrawer'
import axios from 'axios'
export default {
name: 'Index',
components: {
DetectionFilter,
DetectionTools,
DetectionTable,
DetectionDrawer
},
mixins: [dataListMixin],
data () {
return {
// url: api.detection.list,
url: api.knowledgeBase,
listUrl: api.detection.list,
tableId: 'detectionTable',
isNoData: false,
tableData: [],
isSelectedStatus: false,
batchDeleteObjs: [], // 待删除列表
secondBatchDeleteObjs: [],
disableDelete: true,
showConfirmDialog: false,
delItemList: [],
showDrawer: false,
drawerInfo: {}
}
},
methods: {
onSearch () {
// todo 暂时禁用,后续再开发时解禁
// const params = {
// ...this.filterParams,
// name: this.keyWord
// }
// this.clearList()
// this.search(params)
// this.$refs.knowledgeFilter.reloadFilter(this.checkedCategoryIds, this.checkedStatusIds)
},
toDelete (data) {
// todo 暂时禁用,后续再开发时解禁
// if (data && data.ruleId) {
// this.secondBatchDeleteObjs = []
// this.batchDeleteObjs = []
// this.secondBatchDeleteObjs.push(data)
// this.batchDeleteObjs.push(data)
// }
// this.showDelDialog()
},
onCreate () {
// todo 暂时禁用,后续再开发时解禁
// this.$router.push({
// path: '/detection/policies/create',
// query: {
// t: +new Date()
// }
// })
},
selectionChange (objs) {
this.batchDeleteObjs = []
objs.forEach(obj => {
const delObj = this.batchDeleteObjs.find(item => item.ruleId === obj.ruleId)
if (delObj === undefined) {
this.batchDeleteObjs.push(obj)
}
})
this.disableEdit = this.batchDeleteObjs.length !== 1
this.disableDelete = this.batchDeleteObjs.length < 1
},
reloadRowList () {
this.getTableData()
},
toggleLoading () {
},
showDelDialog () {
this.showConfirmDialog = true
this.$nextTick(() => {
this.batchDeleteObjs.forEach((item) => {
this.$refs.delDataTable.toggleRowSelection(item, true)
})
})
},
handleClose () {
this.showConfirmDialog = false
},
secondSelectionChange (objs) {
this.secondBatchDeleteObjs = objs
},
categoryFormat (row, column) {
// const category = row.category
// const t = knowledgeBaseCategory.find(t => t.value === category)
// return t ? t.name : category
},
sourceFormat (row, column) {
// const source = row.source
// const t = knowledgeBaseSource.find(t => t.value === source)
// return t ? t.name : source
},
submit () {
this.delBatchDetection()
this.showConfirmDialog = false
},
delBatchDetection () {
const ids = []
if (this.secondBatchDeleteObjs && this.secondBatchDeleteObjs.length > 0) {
this.secondBatchDeleteObjs.forEach(item => {
ids.push(item.ruleId)
})
}
if (ids.length === 0) {
this.$alert(this.$t('tip.pleaseSelect'), {
confirmButtonText: this.$t('tip.yes'),
type: 'warning'
}).catch(() => {
})
} else {
// todo 调用接口删除
// this.toggleLoading(true)
// axios.delete(api.detection.delete + '?ruleIds=' + ids).then(response => {
// if (response.data.code === 200) {
// this.delFlag = true
// this.$message({ duration: 2000, type: 'success', message: this.$t('tip.deleteSuccess') })
// this.secondBatchDeleteObjs.forEach((item) => {
// this.$refs.delDataTable.toggleRowSelection(item, false)
// })
// this.$refs.knowledgeFilter.reloadFilter()
// this.secondBatchDeleteObjs = []
// this.batchDeleteObjs = []
// delete this.searchLabel.category
// delete this.searchLabel.source
// this.getTableData()
// } else {
// this.$message.error(response.data.message)
// }
// }).finally(() => {
// this.toggleLoading(false)
// if (this.isSelectedStatus != undefined) {
// this.isSelectedStatus = false
// this.disableDelete = true
// this.secondBatchDeleteObjs = []
// this.batchDeleteObjs = []
// this.showConfirmDialog = false
// }
// })
}
},
onRowDoubleClick (data) {
// todo 暂时禁用,后续再开发时解禁
// this.showDrawer = true
// this.drawerInfo = data
}
}
}
</script>
<style lang="scss" scoped>
.detection {
padding: 20px;
height: 100%;
overflow: auto;
.detection-title {
font-family: NotoSansHans-Black;
line-height: 24px;
font-size: 24px;
color: #353636;
font-weight: 900;
display: flex;
flex-direction: column;
.detection-title-label {
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #717171;
letter-spacing: 0;
line-height: 18px;
font-weight: 400;
margin-top: 5px;
}
}
.detection-content {
margin-top: 15px;
width: 100%;
height: calc(100% - 96px);
display: flex;
.detection-filter {
width: 320px;
height: calc(100% + 34px);
background: #FFFFFF;
border: 1px solid rgba(226, 229, 236, 1);
border-radius: 4px;
margin-right: 20px;
}
.detection-block {
width: calc(100% - 340px);
.detection-table {
width: 100%;
height: calc(100% - 44px);
border-radius: 4px;
margin-top: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template >
<div class="entity-filter-case">
<div class="filter-case__header">{{$t('entities.filter')}}</div>
<div
class="entity-filter"
v-for="(filters, index) in filterData"
:key="index"
>
<div class="filter__header">{{filters.title}}</div>
<div class="filter__body">
<div class="filter__row" v-for="(item, i) in filters.data" :key="i">
<el-popover popper-class="filter__row-popover" placement="right-start" :width="440" v-model:visible="item.showTopTen">
<template #reference>
<div class="filter__row-popover" @click="showTopDialog(i, item, filters)">
<div class="row__label">
<i :class="item.icon"></i>
<span>{{item.label}}</span>
</div>
<div class="row__value">
<loading :loading="loadingLeft" size="small"></loading>
<span>{{item.value}}</span>
</div>
</div>
</template>
<entity-top
ref="entityTopTenPop"
:loading="loading"
:popover-data="popoverData"
:item-data="itemData"
:total-count="totalCount"
:top-column="item.topColumn"
@filter="filter"
></entity-top>
</el-popover>
</div>
</div>
</div>
</div>
</template>
<script>
import EntityTop from '@/views/entityExplorer/EntityTop'
import { get } from '@/utils/http'
import { api } from '@/utils/api'
import Loading from '@/components/common/Loading'
import { getSecond } from '@/utils/date-util'
export default {
name: 'EntityFilter',
components: {
Loading,
EntityTop
},
props: {
filterData: Array,
q: String,
timeFilter: Object,
loadingLeft: Boolean
},
data () {
return {
topList: 'list',
topData: [],
entityTopTenData: [],
currentColumn: {},
totalCount: 0,
loading: false,
popoverData: [],
itemData: {}
}
},
watch: {
currentColumn (n, o) {
if (n.column === 'dnsServerOrgCount') {
this.totalCount = this.filterData[3].orgTotalCount
} else if (n.column === 'dnsServerSoftwareCount') {
this.totalCount = this.filterData[3].softwareTotalCount
} else if (n.column === 'dnsServerOsCount') {
this.totalCount = this.filterData[3].osTotalCount
} else if (n.column === 'categoryDistinctCount' && n.type === 'app') {
this.totalCount = this.filterData[1].totalCount
} else {
let count = 0
this.filterData.forEach(f => {
const filter = f.data.some(d => d.column === n.column)
if (filter) {
count = f.totalCount
}
})
this.totalCount = count
}
}
},
methods: {
showTopDialog (i, item, filter) {
if (this.currentColumn.column === item.column && item.showTopTen) {
item.showTopTen = false
return
}
this.filterData.forEach(f => {
f.data.forEach(ff => {
ff.showTopTen = false
})
})
item.showTopTen = true
this.currentColumn = {
column: item.column,
type: filter.type
}
const queryParams = {
q: this.q,
entityType: filter.type,
column: item.topColumn,
top: 10,
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime)
}
this.loading = true
this.popoverData = []
this.itemData = {}
get(api.filterTop, queryParams).then(response => {
if (response.code === 200) {
if (this.currentColumn.column === item.column) {
if (filter.type === 'dns') {
this.popoverData = response.data.result.filter(f => {
return f.count > 0
})
} else {
this.popoverData = response.data.result
}
this.itemData = item
}
} else {
this.popoverData = []
this.itemData = item
}
this.loading = false
}).catch(e => {
this.popoverData = []
this.itemData = item
this.loading = false
})
},
filter (name, topData) {
this.showTopDialog('', topData)
this.$emit('filter', name, topData)
}
}
}
</script>