Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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 LEFT_NAV_CATEGORY_HEADER = "x-left-nav-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,27 @@ 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<ApiCollection> allMcpCollections = ApiCollectionsDao.instance.findAll(mcpTagFilter, null);

// Filter collections based on contextType (cloud vs endpoint)
List<ApiCollection> mcpCollections = allMcpCollections;
if (contextType != null && !contextType.isEmpty()) {
mcpCollections = allMcpCollections.stream()
.filter(collection -> {
boolean isEndpointCollection = collection.getTagsList() != null &&
collection.getTagsList().stream().anyMatch(tag ->
"ENDPOINT".equals(tag.getSource())
);

if ("endpoint".equals(contextType)) {
return isEndpointCollection;
} else { // "cloud" context
return !isEndpointCollection;
}
})
.collect(Collectors.toList());
}

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

switch (filterType) {
Expand Down Expand Up @@ -1224,29 +1262,33 @@ public String fetchMcpdata() {
case "TOOLS":
Bson toolsFilter = Filters.and(
Filters.eq("type", Constants.AKTO_MCP_TOOL),
Filters.ne("remarks", "Rejected")
Filters.ne("remarks", "Rejected"),
mcpCollectionIds.isEmpty() ? Filters.exists("hostCollectionId") : Filters.in("hostCollectionId", mcpCollectionIds)
);
this.mcpDataCount = (int) McpAuditInfoDao.instance.count(toolsFilter);
break;
case "PROMPTS":
Bson promptsFilter = Filters.and(
Filters.eq("type", Constants.AKTO_MCP_PROMPT),
Filters.ne("remarks", "Rejected")
Filters.ne("remarks", "Rejected"),
mcpCollectionIds.isEmpty() ? Filters.exists("hostCollectionId") : Filters.in("hostCollectionId", mcpCollectionIds)
);
this.mcpDataCount = (int) McpAuditInfoDao.instance.count(promptsFilter);
break;
case "RESOURCES":
Bson resourcesFilter = Filters.and(
Filters.eq("type", Constants.AKTO_MCP_RESOURCE),
Filters.ne("remarks", "Rejected")
Filters.ne("remarks", "Rejected"),
mcpCollectionIds.isEmpty() ? Filters.exists("hostCollectionId") : Filters.in("hostCollectionId", mcpCollectionIds)
);
this.mcpDataCount = (int) McpAuditInfoDao.instance.count(resourcesFilter);
break;

case "MCP_SERVER":
Bson mcpServerFilter = Filters.and(
Filters.eq("type", Constants.AKTO_MCP_SERVER),
Filters.ne("remarks", "Rejected")
Filters.ne("remarks", "Rejected"),
mcpCollectionIds.isEmpty() ? Filters.exists("hostCollectionId") : Filters.in("hostCollectionId", mcpCollectionIds)
);
this.mcpDataCount = (int) McpAuditInfoDao.instance.count(mcpServerFilter);
break;
Expand Down Expand Up @@ -1314,11 +1356,28 @@ 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)
);
// Fetch collections with full data to apply context filtering
List<ApiCollection> allGuardRailCollections = ApiCollectionsDao.instance.findAll(guardRailTagFilter, null);

// Filter collections based on contextType (cloud vs endpoint)
List<ApiCollection> guardRailCollections = allGuardRailCollections;
if (contextType != null && !contextType.isEmpty()) {
guardRailCollections = allGuardRailCollections.stream()
.filter(collection -> {
boolean isEndpointCollection = collection.getTagsList() != null &&
collection.getTagsList().stream().anyMatch(tag ->
"ENDPOINT".equals(tag.getSource())
);

if ("endpoint".equals(contextType)) {
return isEndpointCollection;
} else { // "cloud" context
return !isEndpointCollection;
}
})
.collect(Collectors.toList());
}

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 leftNavCategoryFromRequest = httpServletRequest.getHeader(AccessTokenAction.LEFT_NAV_CATEGORY_HEADER);

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

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

if(!StringUtils.isEmpty(leftNavCategoryFromRequest)) {
Context.leftNavCategory.set(leftNavCategoryFromRequest);
}

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 @@ -126,6 +126,7 @@ export default function Header() {
LocalStore.getState().setCategoryMap({});
LocalStore.getState().setSubCategoryMap({});
SessionStore.getState().setThreatFiltersMap({});
PersistStore.getState().setLeftNavCategory('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, LEFT_NAV_CLOUD_SECURITY, LEFT_NAV_ENDPOINT_SECURITY, getLeftNavCategory } 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 leftNavCategory = PersistStore((state) => state.leftNavCategory) || LEFT_NAV_CLOUD_SECURITY;
const setLeftNavCategory = PersistStore((state) => state.setLeftNavCategory);

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 handleLeftNavCategoryChange = (selected) => {
setLeftNavCategory(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") {
setLeftNavCategory("Cloud Security");
} else if (prefix === "endpoint") {
setLeftNavCategory("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") {
setLeftNavCategory("Cloud Security");
} else if (prefix === "endpoint") {
setLeftNavCategory("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, leftNavCategory])

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