Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class AccessTokenAction implements Action, ServletResponseAware, ServletR
public static final String ACCESS_TOKEN_HEADER_NAME = "access-token";
public static final String CONTEXT_SOURCE_HEADER = "x-context-source";
public static final String AKTO_SESSION_TOKEN = "x-akto-session-token";
public static final String SUB_CATEGORY_HEADER = "x-sub-category";

@Override
public String execute() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public class ApiCollectionsAction extends UserAction {
int mcpDataCount;
@Setter
String type;
@Setter
String contextType;
@Getter
List<McpAuditInfo> auditAlerts;

Expand All @@ -116,10 +118,15 @@ public void setApiList(List<ApiInfoKey> apiList) {
* Only populates MCP URLs when dashboard context is not API security.
*/
private void populateCollectionUrls(ApiCollection apiCollection) {
// Do not populate MCP URLs if dashboard context is API security
if (Context.contextSource.get() != null && Context.contextSource.get() == GlobalEnums.CONTEXT_SOURCE.MCP) {
apiCollectionUrlService.populateMcpCollectionUrls(apiCollection);
}else {
try {
// Do not populate MCP URLs if dashboard context is API security
GlobalEnums.CONTEXT_SOURCE contextSource = Context.contextSource.get();
if (contextSource != null && contextSource == GlobalEnums.CONTEXT_SOURCE.MCP) {
apiCollectionUrlService.populateMcpCollectionUrls(apiCollection);
} else {
apiCollection.setUrls(new HashSet<>());
}
} catch (Exception e) {
apiCollection.setUrls(new HashSet<>());
}
}
Expand Down Expand Up @@ -237,7 +244,18 @@ public String fetchApiStats() {
}

public String fetchAllCollectionsBasic() {
UsersCollectionsList.deleteContextCollectionsForUser(Context.accountId.get(), Context.contextSource.get());
try {
// Safely delete context collections for user - handle potential null context source
Integer accountId = Context.accountId.get();
GlobalEnums.CONTEXT_SOURCE contextSource = Context.contextSource.get();
if (accountId != null) {
UsersCollectionsList.deleteContextCollectionsForUser(accountId, contextSource);
}
} catch (Exception e) {
// Log the error but don't fail the entire request
loggerMaker.errorAndAddToDb(e, "Error deleting context collections for user", LogDb.DASHBOARD);
}

this.apiCollections = ApiCollectionsDao.instance.findAll(Filters.empty(), Projections.exclude("urls"));
this.apiCollections = fillApiCollectionsUrlCount(this.apiCollections, Filters.nin(SingleTypeInfo._API_COLLECTION_ID, deactivatedCollections));
return Action.SUCCESS.toUpperCase();
Expand Down Expand Up @@ -1171,7 +1189,9 @@ public String fetchMcpdata() {
Bson mcpTagFilter = Filters.elemMatch(ApiCollection.TAGS_STRING,
Filters.eq("keyName", com.akto.util.Constants.AKTO_MCP_SERVER_TAG)
);

List<ApiCollection> mcpCollections = ApiCollectionsDao.instance.findAll(mcpTagFilter, null);

List<Integer> mcpCollectionIds = mcpCollections.stream().map(ApiCollection::getId).collect(Collectors.toList());

switch (filterType) {
Expand Down Expand Up @@ -1314,11 +1334,13 @@ public String fetchMcpdata() {
Bson guardRailTagFilter = Filters.elemMatch(ApiCollection.TAGS_STRING,
Filters.eq("keyName", Constants.AKTO_GUARD_RAIL_TAG)
);

// Use projection to only fetch IDs, reducing memory usage
List<ApiCollection> guardRailCollections = ApiCollectionsDao.instance.findAll(
guardRailTagFilter,
Projections.include(ApiCollection.ID)
guardRailTagFilter,
Projections.include(ApiCollection.ID)
);

List<Integer> guardRailCollectionIds = guardRailCollections.stream()
.map(ApiCollection::getId)
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
String accessTokenFromResponse = httpServletResponse.getHeader(AccessTokenAction.ACCESS_TOKEN_HEADER_NAME);
String accessTokenFromRequest = httpServletRequest.getHeader(AccessTokenAction.ACCESS_TOKEN_HEADER_NAME);
String contextSourceFromRequest = httpServletRequest.getHeader(AccessTokenAction.CONTEXT_SOURCE_HEADER);
String subCategoryFromRequest = httpServletRequest.getHeader(AccessTokenAction.SUB_CATEGORY_HEADER);

String aktoSessionTokenFromRequest = httpServletRequest.getHeader(AccessTokenAction.AKTO_SESSION_TOKEN);

Expand All @@ -108,6 +109,25 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
}
}

if(!StringUtils.isEmpty(subCategoryFromRequest)) {
try {
// Convert from frontend string format to enum format
GlobalEnums.SUB_CATEGORY_SOURCE subCategoryEnum;
if ("Cloud Security".equalsIgnoreCase(subCategoryFromRequest)) {
subCategoryEnum = GlobalEnums.SUB_CATEGORY_SOURCE.CLOUD_SECURITY;
} else if ("Endpoint Security".equalsIgnoreCase(subCategoryFromRequest)) {
subCategoryEnum = GlobalEnums.SUB_CATEGORY_SOURCE.ENDPOINT_SECURITY;
} else {
// Default fallback
subCategoryEnum = GlobalEnums.SUB_CATEGORY_SOURCE.CLOUD_SECURITY;
}
Context.subCategory.set(subCategoryEnum);
} catch (Exception e) {
// Fallback to default if conversion fails
Context.subCategory.set(GlobalEnums.SUB_CATEGORY_SOURCE.CLOUD_SECURITY);
}
}

if(StringUtils.isNotEmpty(aktoSessionTokenFromRequest) && httpServletRequest.getRequestURI().contains("agent")){
try {
Jws<Claims> claims = JwtAuthenticator.authenticate(aktoSessionTokenFromRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ export default function Header() {
}

const handleDashboardChange = (value) => {
// Preserve current subcategory selection
const currentSubCategory = PersistStore.getState().subCategory || 'Cloud Security';

PersistStore.getState().setAllCollections([]);
PersistStore.getState().setCollectionsMap({});
PersistStore.getState().setHostNameMap({});
Expand All @@ -126,6 +129,15 @@ export default function Header() {
LocalStore.getState().setCategoryMap({});
LocalStore.getState().setSubCategoryMap({});
SessionStore.getState().setThreatFiltersMap({});

// Only set subcategory for dashboard categories that support subcategories
if (value === "MCP Security" || value === "Agentic Security") {
PersistStore.getState().setSubCategory(currentSubCategory);
} else {
// For API Security and other categories, reset to default
PersistStore.getState().setSubCategory('Cloud Security');
}

setDashboardCategory(value);
window.location.reload();
window.location.href("/dashboard/observe/inventory")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import func from "@/util/func";
import Dropdown from "../Dropdown";
import SessionStore from "../../../../main/SessionStore";
import IssuesStore from "../../../pages/issues/issuesStore";
import { CATEGORY_API_SECURITY, mapLabel } from "../../../../main/labelHelper";
import { CATEGORY_API_SECURITY, mapLabel, shouldShowLeftNavSwitch, SUB_CATEGORY_CLOUD_SECURITY, SUB_CATEGORY_ENDPOINT_SECURITY, getSubCategory } from "../../../../main/labelHelper";

export default function LeftNav() {
const navigate = useNavigate();
Expand All @@ -39,11 +39,31 @@ export default function LeftNav() {
const resetStore = LocalStore(state => state.resetStore);
const resetSession = SessionStore(state => state.resetStore);
const resetFields = IssuesStore(state => state.resetStore);
const subCategory = PersistStore((state) => state.subCategory) || SUB_CATEGORY_CLOUD_SECURITY;
const setSubCategory = PersistStore((state) => state.setSubCategory);

const handleSelect = (selectedId) => {
setLeftNavSelected(selectedId);
// Store navigation state in sessionStorage for other components to access
sessionStorage.setItem('leftNavSelected', selectedId);

// Dispatch custom event to notify other components of navigation change
const navigationChangeEvent = new CustomEvent('navigationChanged', {
detail: {
selectedId: selectedId,
timestamp: Date.now()
}
});
window.dispatchEvent(navigationChangeEvent);
};


const handleSubCategoryChange = (selected) => {
setSubCategory(selected);
// Force refresh of current page to reload data with new left nav category
window.location.reload();
};

const handleAccountChange = async (selected) => {
resetAll();
resetStore();
Expand Down Expand Up @@ -85,8 +105,67 @@ export default function LeftNav() {

const dashboardCategory = PersistStore((state) => state.dashboardCategory) || "API Security";

// Helper function to duplicate navigation items with different prefix and independent selection
const duplicateNavItems = (items, prefix, startKey = 100) => {
return items.map((item, index) => {
const newKey = item.key ? `${prefix}_${item.key}` : `${prefix}_${startKey + index}`;
const originalSelected = item.selected;

return {
...item,
key: newKey,
// Each section has independent selection state
selected: originalSelected !== undefined ? leftNavSelected === newKey : undefined,
onClick: item.onClick ? () => {
// Set the left nav category based on section
if (prefix === "cloud") {
setSubCategory("Cloud Security");
} else if (prefix === "endpoint") {
setSubCategory("Endpoint Security");
}

const originalHandler = item.onClick;
originalHandler();
// Set selection specific to this section
handleSelect(newKey);
} : undefined,
subNavigationItems: item.subNavigationItems ? item.subNavigationItems.map((subItem, subIndex) => {
// Create unique keys for sub-navigation items
const originalSubSelected = subItem.selected;
const subNavKey = originalSubSelected ?
leftNavSelected.replace('dashboard_', `${prefix}_dashboard_`) :
`${prefix}_sub_${subIndex}`;

return {
...subItem,
// Sub-items are selected only if they match this section's prefix
selected: originalSubSelected !== undefined ?
leftNavSelected.startsWith(prefix) &&
leftNavSelected === subNavKey
: undefined,
onClick: subItem.onClick ? () => {
// Set the left nav category based on section
if (prefix === "cloud") {
setSubCategory("Cloud Security");
} else if (prefix === "endpoint") {
setSubCategory("Endpoint Security");
}

const originalHandler = subItem.onClick;
originalHandler();
// Extract the base selection pattern and add section prefix
const basePattern = leftNavSelected.replace(/^(cloud_|endpoint_)/, '');
handleSelect(`${prefix}_${basePattern}`);
} : undefined,
};
}) : undefined
};
});
};

const navItems = useMemo(() => {
let items = [
// Account dropdown (stays at the top)
const accountSection = [
{
label: (!func.checkLocal()) ? (
<Box paddingBlockEnd={"2"}>
Expand All @@ -100,6 +179,10 @@ export default function LeftNav() {

) : null
},
];

// Main navigation items (to be duplicated)
const mainNavItems = [
{
label: mapLabel("API Security Posture", dashboardCategory),
icon: ReportFilledMinor,
Expand Down Expand Up @@ -529,25 +612,75 @@ export default function LeftNav() {
}
]
}] : [])
]
];

// Add Quick Start if it doesn't exist
const quickStartItem = {
label: "Quick Start",
icon: AppsFilledMajor,
onClick: () => {
handleSelect("dashboard_quick_start")
navigate("/dashboard/quick-start")
setActive("normal")
},
selected: leftNavSelected === "dashboard_quick_start",
key: "quick_start",
};

const exists = items.find(item => item.key === "quick_start")
const exists = mainNavItems.find(item => item.key === "quick_start");
if (!exists) {
items.splice(1, 0, {
label: "Quick Start",
icon: AppsFilledMajor,
onClick: () => {
handleSelect("dashboard_quick_start")
navigate("/dashboard/quick-start")
setActive("normal")
mainNavItems.splice(0, 0, quickStartItem);
}

// Conditionally create Cloud Security and Endpoint Security sections
// Only show divisions for MCP Security and Agentic Security
const shouldShowDivisions = dashboardCategory === "MCP Security" || dashboardCategory === "Agentic Security";

let allItems;
if (shouldShowDivisions) {
// Create Cloud Security and Endpoint Security sections
const cloudSecurityItems = duplicateNavItems(mainNavItems, "cloud", 200);
const endpointSecurityItems = duplicateNavItems(mainNavItems, "endpoint", 300);

allItems = [
...accountSection,
// Cloud Security Section Header
{
label: (
<Box paddingBlockStart="4" paddingBlockEnd="2">
<div style={{ color: '#000000', fontWeight: 'bold', fontSize: '16px' }}>
Cloud Security
</div>
</Box>
),
key: "cloud_header",
disabled: true,
},
...cloudSecurityItems,
// Endpoint Security Section Header
{
label: (
<Box paddingBlockStart="4" paddingBlockEnd="2">
<div style={{ color: '#000000', fontWeight: 'bold', fontSize: '16px' }}>
Endpoint Security
</div>
</Box>
),
key: "endpoint_header",
disabled: true,
},
selected: leftNavSelected === "dashboard_quick_start",
key: "quick_start",
})
...endpointSecurityItems
];
} else {
// For API Security and other categories, show normal navigation without divisions
allItems = [
...accountSection,
...mainNavItems
];
}

return items
}, [dashboardCategory, leftNavSelected])
return allItems
}, [dashboardCategory, leftNavSelected, subCategory])

const navigationMarkup = (
<div className={active}>
Expand Down
Loading
Loading